RCE的概念与区别

Remote Code Execute 远程代码执行

php
php中可以进行远程代码执行的函数有很多,也常常被一些webshell来做免杀利用。如果我们的输入可以走到以下的函数作为参数,那么就有可能有远程代码执行。

1
2
3
4
5
6
7
eval()//把字符串作为PHP代码执行
assert()//检查一个断言是否为 FALSE,可用来执行代码了
preg_replace()//执行一个正则表达式的搜索和替换
call_user_func()//把第一个参数作为回调函数调用
call_user_func_array()//调用回调函数,并把一个数组参数作为回调函数的参数
array_map()//为数组的每个元素应用回调函数
$a($b)//动态函数

python
python中能进行代码执行的函数也不多。如果我们的输入可以走到以下的函数作为参数,那么就有可能有远程代码执行。

1
2
3
exec(string)# Python代码的动态执行
eval(string)#返回表达式或代码对象的值
execfile(string)#从一个文件中读取和执行Python脚本

java
java中能直接执行代码的函数基本没有,都是调用反序列化来动态执行字符串。

Remote command Execte 远程命令执行

一般出现这种漏洞,是因为应用系统从设计上需要给用户提供指定的远程命令操作的接口。比如我们常见的路由器、防火墙、入侵检测(硬件设备、工业交付)等设备的web管理界面上。仅当Web应用程序代码包含操作系统调用(外壳程序、shel)并且调用中使用了用户输入时才可能进行OS命令注入攻击。它们不是特定于语言的,命令注入漏洞可能会出现在所有让你调用系统外壳命令的语言中:C,Java,PHP,Perl,Ruby,Python等。

其实没啥好讲的,就是一段输入的字符串被引入了执行外部命令的函数,并且没过过滤。

php

1
2
3
4
5
exec - 执行一个外部程序
passthru -执行外部程序并且显示原始输出
proc_open - 执行一个命令,并且打开用来输入/输出的文件指针。
shell_exec - 通过 shell 执行命令并将完整的输出以字符串的方式返回
system - 执行外部程序,并且显示输出

python

1
2
3
os.system()#执行系统指令
os.popen() #popen()方法用于从一个命令打开一个管道
subprocess.call #执行由参数提供的命令

java

1
2
Runtime.getRuntime().exec()
ProcessBuilder()

Shell相关知识

更多的知识可以看这里,其实就是shell的语法

https://www.jianshu.com/p/410cd35e642f

管道

1
command1 | command2 前一个命令的输出作为后一个命令的输入

重定向

1
2
command1 < input.txtinput.txt 的内容读出来重定向作为 command1 的参数 
command2 > out.txt 将command2 的输出重定向到 out.txt

fd
linux下的文件描述符(file descriptor)是linux下一个重要的进程概念(本质上是一个索引)。

我们知道在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件、目录文件、链接文件和设备文件。在操作这些所谓的文件的时候,我们每操作一次就找一次名字,这会耗费大量的时间和效率。所以Linux中规定每一个文件对应一个索引,这样要操作文件的时候,我们直接找到索引就可以对其进行操作了。文件描述符(file descriptor)就是内核为了高效管理这些已经被打开的文件所创建的索引。

1
2
$$    --> linux下当前进程的pid
/proc --> linux伪文件系统 ---》进程相关的信息挂载在这里

linux进程的创建

linux区分用户态和内核态,用户态程序要进行所有动作、其实都是通过调用system call (系统调用syscall)向内核发起请求,最终在内核态执行完毕后才能得到返回.

系统调用 跟用户自定义函数一样也是一个函数,不同的是 系统调用 运行在内核态,而用户自定义函数运行在用户态。由于某些指令(如设置时钟、关闭打开中断和10操作等)只能运行在内核态,所以操作系统必须提供一种能够进入内核态的方式,系统调用 就是这样的一种机制。

linux进程的创建大致是这样一个流程

当执行命令时

1
bash -c whoami

其实在内核态执行了以下动作

1
2
bash(pid:52350)--> sys_fork
--> bash(pid:52796)-->sys_execve --> /bin/whoami(pid:52796)

这一块,关于系统调用的跟进,推荐大家用一个神器来解决,strace。如果你致力于成为一个安全研究员,后面的工作免不了跟他打交道,

看一下strace

执行命令

1
strace -tt-f -e trace=process python3 test.py
1
2
3
4
5
import os
if __name__ =='__main__':
name ='123";ping baidu.com -c 100;echo "456'
cmd ='echo "HELLO '+ name +'"'
os.system(cmd)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[pid 37799] 10:37:14.041412 execve("/bin/sh", ["sh","_C"."echo\"HELL0 123\";ping baidu.com ".'...],0x7ffc96aa0530 /* 24 vars */<unfinished ...>
[pid 37798] 10:37:14.041973 <... clone resumed>)= 37799
[pid 37799]10:37:14.042302<...execve resumed>)= 0
[pid 37798] 10:37:14.043097 wait4(37799,<unfinished4...>
[pid 37799]10:37:14.043152 arch_prctl(0x3001 /* ARCH_??? */,0x7ffcbe43bb00)=-1 EINVAL(Invalid argument)pid 37799]10:37:14.049952 arch_prctI(ARCH_SET_FS,0x7f50d3b85580)=0HELLO 123
[pid 37799] 10:37:14.057025 clone(child_stack-NULL,flagS=CLONE_CHILD_CLEARTIDICLONE_CHILD_SETTIDISIGCHLDstrace: Process37800 attached
,child_tidptr=0x7f50d3b85850)=37800
[pid 37800] 10:37:14.057636 execve("/usr/bin/ping", ["ping","baidu.com","-c","100",0x56153ece58b8 /*24 vars */ <unfinished..>
[pid 37799]10:37:14.057899 wait4(-1, <unfinished ...>
[pid 37800] 10:37:14.057983<...execve resumed>)=0
[pid 37800] 10:37:14.058349 arch_prctl(0x3001 /* ARCH_??? */,0x7ffef4e55b60)=-1 EINVAL (Invalid argument)
[pid 37800] 10:37:14.067113 arch_prctI(ARCH_SET_FS,0x7f7fe32d3040)=0PING baidu.com(110.242.68.66)56(84)bytes of data.
64 bytes from 110.242.68.66(110.242.68.66):icmp_seq=1 ttl-50time=34.8 ms
64 bytes from 110.242.68.66(110.242.68.66):icmp_seq=2 ttl=50time=34.6 ms
64 bytes from 110.242.68.66(110.242.68.66):icmp_seq-3 ttl-50time=34.5 ms
64 bytes from 110.242.68.66(110.242.68.66):icmp_seq=4 ttl-50time=34.8 ms
64 bytes from 110.242.68.66(110.242.68.66):icmp_seq-5 ttl-50time-34.5 ms

c/php/python下的system()/popen()函数

1
2
3
4
5
system($input$)
执行 sh -c '$input'
可以转化为
bash(pid:1) --> sys_fork
--> bash(pid:2)-->sys_execve --> /bin/whoami(pid:2)

python的subprocess.call函数

1
2
3
4
5
6
import os
import subprocess
if _name__='__main__':
name ='123";ping baidu.com -c 100;echo "456'
cmd = 'echo "HELL0 ' + name + '"'
subprocess.call(cmd,shell=True)

1
2
3
4
5
6
import os
import subprocess
if _name__='__main__':
name ='123";ping baidu.com -c 100;echo "456'
cmd = ['echo','"HELL0 ' + name + '"']
subprocess.call(cmd,shell=False)

subprocess.call()可以用数组作为参数运行命令,也可以使用字符串作为参数运行命令(设置参数shell=True时)

这两段代码中,’’shell=False’参数表示在执行命令时不使用shell。而’shell=True’参数则表示使用shell。

  • 当’shell=False’时,’subprocess’模块会直接执行指定的命令,不会调用shell解释器。在这种情况下,命令和参数需要以列表的形式传递给’subprocess.call()’

  • 当’shell=True’时,’subprocess’模块将使用默认shell(如’/bin/sh’ 或 ‘cmd.exe’) , 并将整个命令字符串作为一个参数传递给shell。在这种情况下,命令和参数需要作为一个字符串传递给’subprocess.call()’

  • 在此例子中,如果将’shell=True’传递给’subprocess.call()’,则’name’字符串中的分号’;’,和反引号将被shell解释为命令分隔符和引用字符串。

java的Runtime.getRuntime().exec和ProcessBuilder()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.BufferedReader;
import java.io.I0Exception;
import java.io.InputStream;
import java.io.InputStreamReader;

public class main {
public static void main(String[] args) {
try {
String name="123';ping baidu.com -c 3;echo '456";
String cmd="echo 'HELL0 " + name + "'".
Process pro=Runtime.getRuntime.exec(cmd);
InputStream in = null;
in =pro.getInputStream();
BufferedReader read = new BufferedReader(new InputStreamReader(in));String result= read.readLine();
System.out.println("INF0:"+ result);}
catch(I0Exception e){
throw new RuntimeException(e);
}
}
}

此代码并没有与bash相关,只是执行了 /usr/bin/echo 而参数就是 name 中的字符串

命令注入到底在注入什么

1
2
3
var name = requset.get("name")
var cmd= "echo 'Hello " + name + "'"
RUN_CMD(cmd)

很简单,就是获取一个外部输入,进行简单的拼接,最终放入一个执行外部命令的函数中去执行。这里显然存在一个RCE(远程命令执行)

但是,问题不是这里是否有RCE漏洞。而是
1.上面我们提到的命令执行的函数,都一定存在这个rce吗?
2.如果不一定存在RCE,它们之间的差别是什么?

PHP

1
2
3
4
5
6
7
8
9
10
11
$name = $_GET['name'];
$cmd = 'echo "Hello '.$name.'"';
var_dump("system",system($cmd));//sh -c4echo '</br>';5
var_dump("exec",exec($cmd));//sh-c6
echo '</br>';
var_dump("shell_exec",shell_exec($cmd));//sh -c
echo '</br>':
var_dump("popen");$x=popen($cmd,'r');var_dump($x);var_dump(fread($x,1024));//sh -c
echo '</br>';
$a= array();
var_dump("proc_open");$x=proc_open($cmd,$a,$b); //sh -c

php中大多数执行外部命令的函数,其实都是调用sh -c 去执行

python

1
2
3
4
5
import os
if__name__='_main__':
name ='123";ping baidu.com -c 100;echo "456'
cmd ='echo "HELLO '+ name + '"'
os.system(cmd)

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.BufferedReader;
import java.io.I0Exception;
import java.io.InputStream;
import java.io.InputStreamReader;

public class main {
public static void main(String[] args) {
try {
String name="123';ping baidu.com -c 3;echo '456";
String cmd="echo 'HELL0 " + name + "'".
Process pro=Runtime.getRuntime.exec(cmd); //execve
InputStream in = null;
in =pro.getInputStream();
BufferedReader read = new BufferedReader(new InputStreamReader(in));String result= read.readLine();
System.out.println("INF0:"+ result);}
catch(I0Exception e){
throw new RuntimeException(e);
}
}
}

总结

system类

如果是执行system函数,或者类似system函数,他们都是直接走的fork–>execve流程(调用外部sh -c),这种情况下,我们的输入被拼接加入到作为bash-c的参数,而bash -c是支持shel语法的,所以我们能够很轻易的进行拼接、绕过,这种也是最常见的RCE攻击,简单的一笔。

execve类

比如Runtime.getRuntime().exec()和subprocess.cal(cmd, shell=False)这两者,走的流程是直接execve,在这种情况下,我们的输入只能作为固定进程的参数,那么我们就没办法用shell语法了,与任何拼接都没有关系了。这种情况怎么绕过呢?

参数黑魔法

当我们不能执行任意进程的时候(我们的输入只是某个特定进程的输入的时候)依然能找到一些撸点,但是撸点的大小,取决的使用的进程程序本身的参数是不是能注入

curl可以进行文件读取。

curl支持的协议可以看这里(https://www.cnblogs.com/zhanglianghhh/p/11326428.html)

rpm的黑魔法可以进行rce
app.py

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
44
45
46
47
48
49
import shlex
import subprocess
import urllib

from flask import Flask, request

app = Flask(__name__)


@app.route('/rpm')
def rpm_install(): # put application's code here
rpm = request.args.get("rpm")
if rpm is None or rpm.strip() == "":
return "please input rpm to install"
rpm = urllib.parse.unquote(rpm)
cmd = "rpm " + rpm
cmd_list = shlex.split(cmd)
process = subprocess.Popen(cmd_list, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
result = "result is:\n"
while process.poll() is None:
line = process.stdout.readline()
line = line.strip()
if line:
result = result + str(line)

return result


@app.route('/curl')
def curl(): # put application's code here
url = request.args.get("url")
if url is None or url.strip() == "":
return "please input url"
cmd = ['curl', url]
process = subprocess.Popen(cmd, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
result = "result is:\n"
while process.poll() is None:
line = process.stdout.readline()
line = line.strip()
if line:
result = result + str(line)

return result


if __name__ == '__main__':
app.run(host="0.0.0.0")

上面代码是用python所写的网站app.py
利用方式:

1
rpm --eval '%{lua:os.execute("/bin/sh")}'

https://gtfobins.github.io/