RASP 运行时防护实战:OpenRASP 部署与拦截

RASP 运行时防护(Runtime Application Self-Protection)是在应用进程内部做安全拦截的技术:它以内嵌 Agent 的方式挂载到 JVM 或 PHP 运行时,在函数调用层检测 SQL 注入、命令执行、反序列化、文件上传等危险调用栈。对无法及时打补丁的老系统、外层 WAF 看不透的加密流量场景,RASP 运行时防护是最后一道可控防线。本教程以开源 OpenRASP 为例,给出 Java(Tomcat / Spring Boot)与 PHP 两种部署方式、策略配置与拦截验证。

适用场景

  • 使用第三方商业软件或多年未更新的老代码,无法改源码、无法快速升级组件;
  • 业务全站 HTTPS 或有自定义加密协议,传统 WAF 只能看到加密流量,无法做语义检测;
  • 需要在不改一行业务代码的前提下,为注入类、反序列化类漏洞提供函数级兜底。

前置条件

  • Linux 服务器,具备 root 权限,可重启 Java 应用;
  • Java 8 及以上(OpenRASP Java Agent 支持 JDK 8~17),或 PHP 7.x/8.x;
  • 已准备好测试环境,且明确回滚方式(去掉 javaagent 参数即可)。

原理说明

WAF 工作在 HTTP 流量层,靠正则与语义规则匹配请求报文;RASP 工作在应用运行时层,通过字节码增强(Instrumentation)Hook 关键类与方法,例如 java.sql.Statement.execute、Runtime.exec、ObjectInputStream.readObject、javax.naming.InitialContext.lookup。当这些敏感方法被调用时,Agent 会向上回溯调用栈,把用户可控的 HTTP 参数与当前参数做数据流匹配,命中即按策略记录或阻断。

关键差异有两点:一是位置更深,参数解析完、进数据库之前才判断,绕过编码和变形技巧基本无效;二是上下文更全,能看到完整的 Java 调用栈,因此误报率与检出率都优于纯流量匹配。代价是必须侵入进程、有一定性能开销,且需与业务同生命周期发布。

OpenRASP 采用”Agent + 管理后台(云控)“架构:Agent 负责 Hook 与检测,策略由后台统一下发,支持 off(只记录不拦截)、half(记录并放行)、block(直接阻断)三种模式,便于灰度上线。

操作步骤

1. 下载并解压 Agent

mkdir -p /opt/rasp && cd /opt/rasp
curl -L -o rasp-java.tar.gz \
  https://github.com/baidu/openrasp/releases/latest/download/rasp-java.tar.gz
tar -xzf rasp-java.tar.gz
ls -lh /opt/rasp/rasp/rasp.jar

解压后目录结构为 /opt/rasp/rasp/,包含 rasp.jar 与 conf/ 配置目录。

2. 以 Tomcat 方式挂载 Agent

推荐把启动参数写进 setenv.sh,避免直接改 catalina.sh 造成升级冲突:

cat > /opt/tomcat/bin/setenv.sh <<'EOF'
export JAVA_OPTS="$JAVA_OPTS -javaagent:/opt/rasp/rasp/rasp.jar -Dlog4j.ignoreTCL=true"
export JAVA_OPTS="$JAVA_OPTS -Xms1g -Xmx2g"
EOF
chmod +x /opt/tomcat/bin/setenv.sh

# 重启 Tomcat 使 Agent 生效
/opt/tomcat/bin/shutdown.sh
sleep 5
/opt/tomcat/bin/startup.sh

若是 Spring Boot 独立进程,直接在启动命令中前置参数即可:

java -javaagent:/opt/rasp/rasp/rasp.jar -Dlog4j.ignoreTCL=true -jar app.jar

-Dlog4j.ignoreTCL=true 用于避免与容器内 Log4j 的类加载器产生冲突,是 OpenRASP 官方推荐参数,不要省略。

3. 配置 Agent 策略

cat > /opt/rasp/rasp/conf/rasp.properties <<'EOF'
# 拦截模式:off 只记录 / half 记录并放行 / block 直接阻断
rasp.mode=half

# 日志目录
log.path=/opt/rasp/logs

# 关闭云控,使用本地配置(内网无外网出口时)
cloud.enable=false

# 白名单:属于业务正常逻辑、不应拦截的类与方法
hook.white=com.example.report.ExportService;com.example.batch.SqlRunner;

# 指定不参与 Hook 的包,减少性能开销
hook.ignore=org.springframework.jdbc.datasource.DataSourceUtils;
EOF

mkdir -p /opt/rasp/logs && chmod 750 /opt/rasp/logs

灰度阶段建议先设 rasp.mode=half,观察 3~7 天业务告警,确认无误报后再切到 block。切到 block 后,命中规则的请求会被 Agent 直接 throw 异常,接口返回 500 或 501。

4. PHP 环境部署(可选)

curl -L -o rasp-php.tar.gz \
  https://github.com/baidu/openrasp/releases/latest/download/rasp-php.tar.gz
tar -xzf rasp-php.tar.gz -C /opt/rasp
/opt/rasp/rasp-php/install.sh -i

# 安装脚本会提示把 openrasp.so 写入 php.ini
echo "extension=/opt/rasp/rasp-php/openrasp.so" >> /etc/php.ini
echo "openrasp.root_dir=/opt/rasp/rasp-php" >> /etc/php.ini
systemctl restart php-fpm

5. 灰度与性能观察

# 观察 Agent 加载与拦截日志
tail -f /opt/rasp/logs/rasp/rasp.log
tail -f /opt/rasp/logs/alarm/alarm.log

# 评估开销:对比挂载前后的接口 P95 耗时
ab -n 2000 -c 50 https://app.example.com/api/health

实测中 RASP 对普通业务接口的额外耗时通常在 1%~5% 量级;若 P95 明显上升,优先用 hook.ignore 排除高频非危险调用,而不是直接关掉 Agent。

配置验证

# 1. 确认 Agent 已加载:启动日志中应有 OpenRASP 版本横幅
grep -i "openrasp" /opt/tomcat/logs/catalina.out | head -n 5

# 2. 发起一次模拟 SQL 注入,观察是否产生告警
curl -s "http://127.0.0.1:8080/user?id=1%20or%201=1--"

# 3. 查看告警 JSON(含命中规则、调用栈、参数值)
ls -lt /opt/rasp/logs/alarm/ | head
cat /opt/rasp/logs/alarm/$(ls -t /opt/rasp/logs/alarm/ | head -n 1) | python -m json.tool

# 4. 切到 block 后复测:请求应被阻断而非返回正常页面
curl -s -o /dev/null -w "http_code=%{http_code}\n" "http://127.0.0.1:8080/user?id=1%20or%201=1--"

预期结果:第 1 步能看到 OpenRASP 版本横幅;第 2 步在 alarm 目录生成含 sql 事件类型的新文件;第 4 步在 block 模式下 http_code 变为 500 或 501,同时业务正常请求仍返回 200。

检测能力与边界

OpenRASP 的检测插件按攻击类型划分,覆盖面如下:

  • SQL 注入:Hook Statement.execute* 与 PreparedStatement 参数绑定,比对参数值是否与 HTTP 输入同源;
  • 命令执行:Hook Runtime.exec、ProcessBuilder.start,识别分号、管道、反引号拼接;
  • 文件操作:Hook 文件读写与解压相关方法,检测上传目录跨越与危险后缀;
  • 反序列化与 JNDI:Hook ObjectInputStream.readObject、InitialContext.lookup,对 Fastjson、Log4j2 一类漏洞形成兜底。

需要明确的边界是:RASP 依赖 Hook 到具体类与方法,若业务自行实现了协议解析、或把危险操作下沉到 JNI 与 Native 层,Agent 无法覆盖;且它只做运行时兜底,不能替代代码修复与依赖升级。正确姿势是三者并行——外层 WAF 拦流量、RASP 兜运行时、补丁与升级负责根治。

防止 Agent 被误摘除

Agent 生效的唯一凭证是启动参数,运维一次”精简启动脚本”就可能让防护静默失效。建议加一条定时校验并接入告警:

# 校验运行中的 JVM 是否仍带 -javaagent 参数
if ! jcmd $(pgrep -f app.jar) VM.command_line 2>/dev/null | grep -q "rasp.jar"; then
    echo "[ALERT] RASP agent missing on $(hostname) at $(date '+%F %T')" \
      | tee -a /var/log/rasp-watch.log
fi

把该脚本放进 crontab 每分钟执行,出现告警即通知到监控平台。同时在发布检查项中明确写入”启动参数不得移除 -javaagent“,从流程上避免防护被无意关掉。

常见问题(FAQ)

Q1:加了 javaagent 后应用启动失败或端口被占

常见三种原因:一是 rasp.jar 路径写错或文件损坏,先用 ls -lh 确认文件存在且大小正常;二是 Agent 与应用的日志组件冲突,补上 -Dlog4j.ignoreTCL=true;三是 Agent 初始化时占用了默认端口,与业务端口撞车,改用 cloud.enable=false 关闭云控即可避开。启动失败时回滚只需删掉 setenv.sh 中的 -javaagent 段并重启。

Q2:上线后业务接口大面积 500,如何快速降级与加白

先把 rasp.mode 从 block 改为 half 并重启应用,服务立即恢复放行但仍保留告警记录。随后到 alarm 日志中定位被拦截的类与方法,写入 hook.white 白名单,例如 hook.white=com.example.batch.SqlRunner;。加白要精确到类,避免直接写包名导致整包绕过。确认白名单生效且不再误报后,再切回 block。

Q3:拦截确实生效,但只能事后告警、无法阻断

先确认 rasp.mode 已是 block 并重启进程,配置改动不会热加载。若仍不阻断,检查应用是否使用了自定义数据库连接池或 ORM 直接操作原生连接,导致 OpenRASP 需要 Hook 的类没被覆盖;此时可在 hook.ignore 中移除对相关数据源的排除项。RASP 无法覆盖插件化、纯内存计算的逻辑,这类场景仍需配合外层 WAF 做流量侧拦截。

总结

RASP 运行时防护的价值在于贴近数据流、难被编码绕过,与 WAF 形成纵深互补:WAF 在外层拦掉扫描与通用攻击,RASP 在内层兜住注入与反序列化。落地关键是三步——用 -javaagent 挂载 Agent、以 half 模式灰度观察、按告警精确维护 hook.white 白名单,最后再切 block。发布节奏必须与业务同步,任何策略变更都要预留一键摘除 Agent 的回滚路径。