JAVA EE初阶 - 预备知识(一)
一、管道
在计算机编程和操作系统环境中,输入、输出和错误管道是用于在不同进程或程序之间传递数据和信息的重要机制,下面分别对它们进行详细介绍:
输入管道(Standard Input,stdin)
- 定义:输入管道是进程获取外部数据的通道。它允许一个进程从其他进程、用户输入设备(如键盘)或文件中接收数据。当一个进程启动时,操作系统通常会为其分配一个标准输入流,进程可以通过读取这个输入流来获取所需的数据。
- 工作原理:在命令行环境中,用户可以通过键盘输入数据,这些数据会被发送到当前正在运行的程序的标准输入管道中。程序可以使用相应的函数或方法来读取输入管道中的数据。例如,在 Python 中,可以使用
input()
函数从标准输入读取用户输入的一行文本;在 Java 中,可以使用Scanner
类来读取标准输入。 - 示例(Python):
# 从标准输入读取用户输入
user_input = input("请输入一些内容: ")
print(f"你输入的内容是: {user_input}")
- 管道连接示例:在 Unix/Linux 系统中,可以使用管道符号
|
来连接多个命令,将前一个命令的输出作为后一个命令的输入。例如,ls -l | grep .txt
命令将ls -l
命令的输出通过管道传递给grep .txt
命令,grep
命令从其输入管道中读取数据并进行筛选。
输出管道(Standard Output,stdout)
- 定义:输出管道是进程向外部发送数据的通道。进程可以将计算结果、提示信息等数据通过输出管道发送到其他进程、文件或显示设备(如显示器)。当一个进程启动时,操作系统会为其分配一个标准输出流,进程可以将数据写入这个输出流。
- 工作原理:程序可以使用相应的函数或方法将数据写入输出管道。在 Python 中,可以使用
print()
函数将数据输出到标准输出;在 Java 中,可以使用System.out.println()
方法。 - 示例(Java):
public class OutputExample {
public static void main(String[] args) {
// 向标准输出打印信息
System.out.println("这是一个输出示例");
}
}
- 重定向示例:在命令行中,可以使用重定向符号
>
将命令的输出重定向到文件中。例如,ls -l > file_list.txt
命令将ls -l
命令的输出重定向到file_list.txt
文件中,而不是显示在屏幕上。
错误管道(Standard Error,stderr)
- 定义:错误管道用于进程输出错误信息和诊断信息。当进程在执行过程中遇到错误或异常时,会将相关的错误信息通过错误管道发送出去,以便用户或其他程序能够及时了解问题所在。与标准输出管道不同,错误管道专门用于处理错误情况,这样可以将正常输出和错误信息分开处理。
- 工作原理:程序在捕获到错误或异常时,可以将错误信息写入错误管道。在 Python 中,可以使用
sys.stderr
来输出错误信息;在 Java 中,可以使用System.err.println()
方法。 - 示例(Python):
import sys
try:
result = 1 / 0
except ZeroDivisionError:
# 向标准错误输出错误信息
print("发生了除零错误", file=sys.stderr)
- 分离输出和错误示例:在命令行中,可以使用不同的重定向符号将标准输出和错误输出分别重定向到不同的文件。例如,
ls non_existent_file 2> error.log 1> output.log
命令将错误信息重定向到error.log
文件,将正常输出重定向到output.log
文件。
管道在进程间通信中的作用
输入、输出和错误管道在进程间通信中起着关键作用,它们允许不同的进程之间进行数据交换和信息传递,使得多个进程可以协同工作,完成更复杂的任务。通过管道,一个进程的输出可以作为另一个进程的输入,形成一个处理链,提高了系统的灵活性和可扩展性。
二、脚本语言漏洞举例(乱入)
1.Python 的 subprocess
模块
概述
subprocess
模块是 Python 标准库中用于创建新进程、连接到它们的输入 / 输出 / 错误管道,并获取它们的返回码的模块。它提供了一个更强大且灵活的方式来执行外部命令,是 Python 中替代旧的 os.system()
、os.popen()
等函数的推荐方法。
常用方法和功能
subprocess.run()
:这是 Python 3.5 及以上版本中用于执行外部命令的高级接口。它会等待命令执行完成,并返回一个CompletedProcess
对象,该对象包含了命令的返回码、标准输出和标准错误等信息。
import subprocess
result = subprocess.run(['ls', '-l'], capture_output=True, text=True)
if result.returncode == 0:
print(result.stdout)
else:
print(result.stderr)
subprocess.Popen()
:这是一个更底层的接口,允许你更精细地控制子进程的执行。你可以手动管理子进程的输入、输出和错误流,还可以在命令执行过程中进行交互。
import subprocess
process = subprocess.Popen(['ping', 'www.example.com'], stdout=subprocess.PIPE, text=True)
while True:
output = process.stdout.readline()
if output == '' and process.poll() is not None:
break
if output:
print(output.strip())
安全风险
如果在使用 subprocess
模块时直接将用户输入作为命令的一部分,而没有进行充分的验证和过滤,就可能会导致 OS 注入攻击。例如:
import subprocess
user_input = input("请输入要执行的命令参数: ")
# 不安全的写法,可能导致命令注入
subprocess.run(f'ls {user_input}', shell=True)
2.Java 的 Runtime.exec()
方法
概述
Runtime.exec()
是 Java 中用于执行外部系统命令的方法。Runtime
类代表 Java 应用程序运行时的环境,通过 Runtime.getRuntime()
方法可以获取当前 Java 虚拟机的运行时对象,然后调用 exec()
方法来执行外部命令。
常用调用方式
- 传递字符串参数:可以直接传递一个包含完整命令的字符串给
exec()
方法。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class RuntimeExecExample {
public static void main(String[] args) {
try {
Process process = Runtime.getRuntime().exec("ls -l");
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
int exitCode = process.waitFor();
System.out.println("命令执行完毕,返回码: " + exitCode);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
- 传递字符串数组参数:也可以传递一个字符串数组,数组的每个元素代表命令的一个部分。
Process process = Runtime.getRuntime().exec(new String[]{"ls", "-l"});
安全风险
同样,如果在使用 Runtime.exec()
方法时将用户输入直接拼接到命令中,而没有进行严格的验证和过滤,会存在 OS 注入的风险。例如:
import java.io.IOException;
import java.util.Scanner;
public class UnsafeRuntimeExec {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要执行的命令参数: ");
String userInput = scanner.nextLine();
try {
// 不安全的写法,可能导致命令注入
Runtime.getRuntime().exec("ls " + userInput);
} catch (IOException e) {
e.printStackTrace();
}
}
}
三、OS注入攻击(再次乱入)
OS 注入攻击,即操作系统注入攻击,是一种恶意攻击手段,攻击者通过利用应用程序或系统中的漏洞,将恶意的操作系统命令注入到目标系统中执行,以获取对系统的控制权或执行其他恶意操作。以下是关于它的详细介绍:
攻击原理
- 输入验证缺失:与 SQL 注入类似,很多应用程序会接收用户输入并将其用于执行系统命令或与操作系统进行交互。如果应用程序没有对用户输入进行严格的验证和过滤,攻击者就可以输入包含恶意操作系统命令的内容,这些命令会被操作系统当作正常命令执行。
- 命令拼接漏洞:在一些情况下,应用程序使用字符串拼接的方式来构建要执行的系统命令。攻击者通过注入恶意命令片段,改变原本的命令逻辑,从而执行他们期望的操作。例如,在一个执行文件下载的功能中,正常的命令可能是
wget https://example.com/file.txt
,攻击者可以通过在输入字段中输入恶意内容,如; rm -rf /
,使得最终执行的命令变成wget https://example.com/file.txt; rm -rf /
,从而导致系统中的文件被删除。
攻击方式
- 通过 Web 应用漏洞:如果 Web 应用存在漏洞,攻击者可以通过在 Web 页面的输入框、表单等位置输入恶意命令,利用 Web 应用与操作系统的交互来执行注入的命令。比如在一个文件上传功能中,攻击者可以构造包含恶意命令的文件名,当服务器对文件名进行处理并执行相关系统命令时,就可能触发 OS 注入。
- 利用网络服务漏洞:一些网络服务在处理用户请求时可能存在漏洞,攻击者可以向这些服务发送特制的请求,其中包含恶意的操作系统命令。例如,某些 FTP 服务器在处理用户的命令输入时,如果没有进行充分的验证,攻击者就可以通过 FTP 命令注入恶意的操作系统指令。
- 利用脚本语言漏洞:在使用脚本语言编写的应用程序中,如果对用户输入的处理不当,也可能导致 OS 注入攻击。例如,在 Python 的
subprocess
模块或 Java 的Runtime.exec()
方法中,如果直接使用用户输入来执行系统命令,而没有进行安全检查,就容易受到攻击。
危害
- 系统控制与数据窃取:攻击者可以获取系统的管理员权限或其他高级权限,从而能够访问、窃取系统中的敏感数据,如用户账号信息、机密文件等。
- 系统破坏与服务中断:能够删除重要的系统文件、篡改系统配置,导致系统无法正常运行,使服务中断,影响业务的连续性,给企业和用户带来严重的损失。
- 进一步攻击的跳板:一旦攻击者成功实施 OS 注入攻击并控制了目标系统,就可以将该系统作为跳板,进一步对内部网络中的其他系统进行攻击,扩大攻击范围。
防范措施
- 严格输入验证:对所有来自用户的输入进行严格的验证和过滤,确保输入只包含合法的字符和内容,不包含任何可能导致命令注入的特殊字符或命令关键字。
- 使用安全的函数和方法:在编写代码时,优先使用安全的函数和方法来执行系统命令或与操作系统交互。这些函数通常会对输入进行自动的安全处理,减少注入风险。
- 最小权限原则:为应用程序和用户分配最小的权限,只授予其执行必要操作所需的权限,避免使用具有过高权限的账户来运行应用程序或执行系统命令。
- 系统更新与漏洞修复:及时更新操作系统、应用程序和相关软件,修复已知的安全漏洞,降低被攻击的风险。
四、进程间通信的方式
进程间通信(Inter - Process Communication,IPC)是指在不同进程之间传播或交换信息的机制。以下是常见的进程间通信方式:
1. 管道(Pipe)
- 匿名管道(Anonymous Pipe)
- 原理:一种半双工的通信方式,数据只能在一个方向上流动,通常用于具有亲缘关系的进程(如父子进程)之间。在创建管道时,操作系统会返回两个文件描述符,一个用于读,一个用于写。
- 示例(Unix/Linux 系统,C 语言):父进程向子进程发送数据
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
int pipefd[2];
pid_t pid;
char buffer[100];
if (pipe(pipefd) == -1) {
perror("pipe");
return 1;
}
pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) { // 子进程
close(pipefd[1]); // 关闭写端
read(pipefd[0], buffer, sizeof(buffer));
printf("子进程收到: %s\n", buffer);
close(pipefd[0]);
} else { // 父进程
close(pipefd[0]); // 关闭读端
const char *message = "Hello, child process!";
write(pipefd[1], message, strlen(message));
close(pipefd[1]);
}
return 0;
}
- 命名管道(Named Pipe,FIFO)
- 原理:一种全双工的通信方式,它可以在不相关的进程之间进行通信。命名管道以文件的形式存在于文件系统中,进程通过打开和操作这个文件来进行数据传输。
- 示例(Unix/Linux 系统,C 语言):一个进程写数据,另一个进程读数据
// 写进程
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#define FIFO_NAME "myfifo"
int main() {
int fd;
const char *message = "Hello, named pipe!";
fd = open(FIFO_NAME, O_WRONLY);
write(fd, message, strlen(message));
close(fd);
return 0;
}
// 读进程
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define FIFO_NAME "myfifo"
int main() {
int fd;
char buffer[100];
fd = open(FIFO_NAME, O_RDONLY);
read(fd, buffer, sizeof(buffer));
printf("收到消息: %s\n", buffer);
close(fd);
return 0;
}
2. 消息队列(Message Queue)
- 原理:消息队列是存放在内核中的消息链表,每个消息队列由一个标识符(即队列 ID)来标识。进程可以向消息队列中添加消息,也可以从消息队列中读取消息。消息队列可以实现消息的随机查询,不一定要以先进先出的顺序读取。
- 示例(Python,使用
multiprocessing.Queue
)
from multiprocessing import Process, Queue
def sender(q):
q.put("Hello from sender!")
def receiver(q):
message = q.get()
print(f"Receiver got: {message}")
if __name__ == '__main__':
q = Queue()
p1 = Process(target=sender, args=(q,))
p2 = Process(target=receiver, args=(q,))
p1.start()
p2.start()
p1.join()
p2.join()
3. 共享内存(Shared Memory)
- 原理:多个进程可以访问同一块物理内存区域,是最快的一种 IPC 方式。因为数据不需要在不同的进程之间复制,而是直接在共享内存区域进行读写操作。
- 示例(Unix/Linux 系统,C 语言)
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#define SHM_SIZE 1024
int main() {
int shmid;
key_t key;
char *shm, *s;
key = ftok(".", 'a');
shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
shm = shmat(shmid, NULL, 0);
strcpy(shm, "Hello, shared memory!");
s = shm;
while (*s != '\0') {
putchar(*s++);
}
putchar('\n');
shmdt(shm);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
4.信号量(Semaphore)
- 原理:信号量是一个计数器,用于控制多个进程对共享资源的访问。它主要用于实现进程间的同步和互斥。当信号量的值大于 0 时,表示可以访问共享资源;当信号量的值为 0 时,表示需要等待其他进程释放资源。
- 示例(Unix/Linux 系统,C 语言)
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
void sem_init(int semid) {
union semun arg;
arg.val = 1;
if (semctl(semid, 0, SETVAL, arg) == -1) {
perror("semctl");
exit(1);
}
}
void sem_wait(int semid) {
struct sembuf sb = {0, -1, 0};
if (semop(semid, &sb, 1) == -1) {
perror("semop");
exit(1);
}
}
void sem_signal(int semid) {
struct sembuf sb = {0, 1, 0};
if (semop(semid, &sb, 1) == -1) {
perror("semop");
exit(1);
}
}
int main() {
int semid;
key_t key;
key = ftok(".", 'a');
semid = semget(key, 1, IPC_CREAT | 0666);
sem_init(semid);
sem_wait(semid);
printf("进入临界区\n");
sem_signal(semid);
printf("离开临界区\n");
semctl(semid, 0, IPC_RMID);
return 0;
}
5.套接字(Socket)
- 原理:套接字是一种网络编程接口,它不仅可以用于不同主机之间的进程通信,也可以用于同一主机上的进程通信。通过创建套接字,进程可以建立网络连接,发送和接收数据。
- 示例(Python,本地进程间通信)
# 服务器端
import socket
server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server_socket.bind('./uds_socket')
server_socket.listen(1)
conn, addr = server_socket.accept()
data = conn.recv(1024)
print(f"收到消息: {data.decode()}")
conn.close()
server_socket.close()
# 客户端
import socket
client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client_socket.connect('./uds_socket')
message = "Hello, socket!"
client_socket.sendall(message.encode())
client_socket.close()
五、进程间通信方式的选择依据是什么?
在选择进程间通信(IPC)方式时,需要综合考虑多个因素,以下是一些主要的选择依据:
通信需求方面
- 数据传输量
- 少量数据:如果进程间只需要传输少量的数据,如状态信息、标志位等,管道(包括匿名管道和命名管道)、消息队列是比较合适的选择。例如,在一个简单的父子进程通信场景中,父进程向子进程发送一个启动信号,使用匿名管道就可以高效地完成。
- 大量数据:当需要传输大量数据时,共享内存是最佳选择。因为共享内存直接在物理内存中开辟一块区域供多个进程访问,数据不需要在进程间复制,避免了数据复制带来的开销,能显著提高数据传输效率。比如,在图像处理应用中,一个进程负责采集图像数据,另一个进程负责对图像进行处理,使用共享内存可以快速地将采集到的大量图像数据传递给处理进程。
- 数据传输方向
- 单向传输:匿名管道是一种半双工的通信方式,数据只能在一个方向上流动,适合单向的数据传输场景。例如,在一个日志处理系统中,一个进程负责生成日志,另一个进程负责将日志写入文件,使用匿名管道可以方便地将日志数据从生成进程传输到写入进程。
- 双向传输:命名管道、套接字和共享内存都支持双向的数据传输。如果两个进程需要频繁地进行数据交互,例如客户端 - 服务器架构中的客户端和服务器进程,使用套接字可以实现双向的通信,并且可以在不同主机之间进行通信。
- 数据传输的实时性
- 实时要求高:共享内存和套接字更适合对实时性要求较高的场景。共享内存由于直接操作物理内存,数据传输几乎没有延迟;套接字可以通过设置合适的网络参数和协议,保证数据的实时传输。例如,在实时监控系统中,传感器数据需要及时传输到处理进程进行分析,使用共享内存或套接字可以满足实时性要求。
- 实时要求低:消息队列对实时性的要求相对较低,它允许消息在队列中进行缓冲和排队。例如,在一个邮件系统中,用户发送的邮件可以先放入消息队列中,然后由后台进程逐步处理和发送,不需要立即处理。
进程关系方面
- 亲缘关系进程:对于具有亲缘关系的进程(如父子进程),匿名管道是一种简单且高效的通信方式。因为匿名管道的创建和使用与进程的亲缘关系紧密相关,父进程创建管道后可以方便地将文件描述符传递给子进程。例如,在一个多进程的文件处理程序中,父进程可以通过匿名管道将文件的部分内容传递给子进程进行处理。
- 非亲缘关系进程:命名管道、消息队列、共享内存和套接字可以用于非亲缘关系的进程之间的通信。其中,套接字不仅可以用于同一主机上的非亲缘关系进程通信,还可以用于不同主机之间的进程通信,具有更广泛的适用性。例如,在一个分布式系统中,不同服务器上的进程可以通过套接字进行通信和数据交换。
同步与互斥需求方面
- 需要同步互斥:如果多个进程需要对共享资源进行同步访问,避免数据竞争和不一致问题,信号量是一个重要的选择。信号量可以作为一种同步机制,控制进程对共享资源的访问顺序。例如,在一个多进程的数据库管理系统中,多个进程可能同时需要访问数据库中的某个表,使用信号量可以确保同一时间只有一个进程可以对该表进行写操作,保证数据的一致性。
- 不需要同步互斥:如果进程间的通信不需要严格的同步和互斥机制,如简单的数据传递场景,可以选择管道或消息队列等方式。这些方式相对简单,不需要额外的同步操作。
系统兼容性和可移植性方面
- 特定系统:不同的操作系统对各种 IPC 方式的支持程度可能有所不同。例如,某些实时操作系统可能对共享内存和信号量的支持更好,而在一些嵌入式系统中,可能更倾向于使用简单的管道或消息队列。在选择 IPC 方式时,需要考虑目标系统的特性和限制。
- 跨平台需求:如果应用程序需要在不同的操作系统上运行,套接字是一个比较好的选择,因为它具有良好的跨平台性。大多数操作系统都提供了对套接字编程的支持,并且可以通过标准的网络协议进行通信。
开发复杂度方面
- 简单开发:管道和消息队列的使用相对简单,不需要复杂的编程和配置。对于初学者或对开发时间要求较高的项目,可以优先考虑使用这些方式。例如,在一个小型的脚本化应用中,使用管道可以快速实现进程间的通信。
- 复杂功能:共享内存和套接字的使用相对复杂,需要处理更多的细节,如内存管理、网络连接和错误处理等。但它们可以实现更复杂的功能,如高性能的数据传输和分布式通信。在开发大型的分布式系统或对性能要求较高的应用时,可能需要选择这些方式。