PF的代码审计日记_CTF篇_DAY_0


前言

自己代码审计太差了,决定开始一波学习,就先从CTF开始了,虽然之前刷过沙雕CVE,不过是直接危险函数,来快速找的,没有什么意义,不过挺大佬说,代码审计是不需要环境搭建的,纯白盒,所以这次就不黑白结合了。(虽然比赛都是黑白结合这样搞的)

DDCTF Web签到题

先从简单的开始

  • Application.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
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    <?php
    Class Application {
    var $path = '';


    public function response($data, $errMsg = 'success') {
    $ret = ['errMsg' => $errMsg,
    'data' => $data];
    $ret = json_encode($ret);
    header('Content-type: application/json');
    echo $ret;

    }

    public function auth() {
    $DIDICTF_ADMIN = 'admin';
    if(!empty($_SERVER['HTTP_DIDICTF_USERNAME']) && $_SERVER['HTTP_DIDICTF_USERNAME'] == $DIDICTF_ADMIN) {
    $this->response('您当前当前权限为管理员----请访问:app/fL2XID2i0Cdh.php');
    return TRUE;
    }else{
    $this->response('抱歉,您没有登陆权限,请获取权限后访问-----','error');
    exit();
    }

    }
    private function sanitizepath($path) {
    $path = trim($path);
    $path=str_replace('../','',$path);
    $path=str_replace('..\\','',$path);
    return $path;
    }

    public function __destruct() {
    if(empty($this->path)) {
    exit();
    }else{
    $path = $this->sanitizepath($this->path);
    if(strlen($path) !== 18) {
    exit();
    }
    $this->response($data=file_get_contents($path),'Congratulations');
    }
    exit();
    }
    }
    ?>
  • Session.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
    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
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    <?php
    include 'Application.php';
    class Session extends Application {

    //key建议为8位字符串
    var $eancrykey = '';
    var $cookie_expiration = 7200;
    var $cookie_name = 'ddctf_id';
    var $cookie_path = '';
    var $cookie_domain = '';
    var $cookie_secure = FALSE;
    var $activity = "DiDiCTF";


    public function index()
    {
    if(parent::auth()) {
    $this->get_key();
    if($this->session_read()) {
    $data = 'DiDI Welcome you %s';
    $data = sprintf($data,$_SERVER['HTTP_USER_AGENT']);
    parent::response($data,'sucess');
    }else{
    $this->session_create();
    $data = 'DiDI Welcome you';
    parent::response($data,'sucess');
    }
    }

    }

    private function get_key() {
    //eancrykey and flag under the folder
    $this->eancrykey = file_get_contents('../config/key.txt');
    }

    public function session_read() {
    if(empty($_COOKIE)) {
    return FALSE;
    }

    $session = $_COOKIE[$this->cookie_name];
    if(!isset($session)) {
    parent::response("session not found",'error');
    return FALSE;
    }
    $hash = substr($session,strlen($session)-32);
    $session = substr($session,0,strlen($session)-32);

    if($hash !== md5($this->eancrykey.$session)) {
    parent::response("the cookie data not match",'error');
    return FALSE;
    }
    $session = unserialize($session);


    if(!is_array($session) OR !isset($session['session_id']) OR !isset($session['ip_address']) OR !isset($session['user_agent'])){
    return FALSE;
    }

    if(!empty($_POST["nickname"])) {
    $arr = array($_POST["nickname"],$this->eancrykey);
    $data = "Welcome my friend %s";
    foreach ($arr as $k => $v) {
    $data = sprintf($data,$v);
    }
    parent::response($data,"Welcome");
    }

    if($session['ip_address'] != $_SERVER['REMOTE_ADDR']) {
    parent::response('the ip addree not match'.'error');
    return FALSE;
    }
    if($session['user_agent'] != $_SERVER['HTTP_USER_AGENT']) {
    parent::response('the user agent not match','error');
    return FALSE;
    }
    return TRUE;

    }

    private function session_create() {
    $sessionid = '';
    while(strlen($sessionid) < 32) {
    $sessionid .= mt_rand(0,mt_getrandmax());
    }

    $userdata = array(
    'session_id' => md5(uniqid($sessionid,TRUE)),
    'ip_address' => $_SERVER['REMOTE_ADDR'],
    'user_agent' => $_SERVER['HTTP_USER_AGENT'],
    'user_data' => '',
    );

    $cookiedata = serialize($userdata);
    $cookiedata = $cookiedata.md5($this->eancrykey.$cookiedata);
    $expire = $this->cookie_expiration + time();
    setcookie(
    $this->cookie_name,
    $cookiedata,
    $expire,
    $this->cookie_path,
    $this->cookie_domain,
    $this->cookie_secure
    );

    }
    }


    $ddctf = new Session();
    $ddctf->index();
    ?>

首先从Session.php可以看到是继承了Application这个类,所以我们先从Application这个类开始

Function:

* repponse:
        通过json返回数据,设置一下Content-type: Pass
* auth:
        设置变量为admin,若`$_SERVER['HTTP_DIDICTF_USERNAME']`是admin的话就返回True,并告诉你一段信息(其实这里就是原题中获取源码的方式)
* sanitizepath:
        将传入的path变量先全部转换为小写,再讲../和..\\变为空
* __destruct:
    在类被销毁时触发,会将path变量先过一遍上面的函数,然后再返回path变量对应的文件内容,不过path变量长度必须为18.(这里就是很明显的作者给我们用来反序列化任意读取的点了)

现在看Session这个类,这里面就有很多的方法了,也比较长,那么我们就跟着流程来,首先是$ddctf = new Session();$ddctf->index();

我们先从index()方法跟进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function index()
{
if(parent::auth()) {
$this->get_key();
if($this->session_read()) {
$data = 'DiDI Welcome you %s';
$data = sprintf($data,$_SERVER['HTTP_USER_AGENT']);
parent::response($data,'sucess');
}else{
$this->session_create();
$data = 'DiDI Welcome you';
parent::response($data,'sucess');
}
}

}

  1. 调用了自己父类的auth上面已经提到过了,只需要$_SERVER['HTTP_DIDICTF_USERNAME'] == 'admin'即可,由此进入判断
  2. 调用get_key
  3. 进入新的判断,若session_read通过即发出欢迎,失败则进入session_create,创建session成功后,也会欢迎

跟进getkey:
../config/key.txt读出密钥

跟进session_read()

1. 判断是否设置设置cookie : `ddctf_id`作为session
2. session的后32位作为hash,其余作为session本身
3. 如果`$hash !== md5($this->eancrykey.$session`就返回错误
4. 然后就反序列化session(联系之前的就是反序列化的触发点)
5. 反序列化后,会判断数组session是否有各项信息
6. 如果POST变量`nickname`,则会生成一个数组,和密钥在一起,经过一个格式化字符串之后输出,不过`Welcome my friend %s`在第一轮后就会变成`Welcome my friend $nickname`第二轮的sprintf就无效了

跟进session_create

1. 设置随机32位sessionid,设置各种信息
2. 序列化`session`数组,然后 `$cookiedata = $cookiedata.md5($this->eancrykey.$cookiedata);`通过密钥来进行签名
  • 看完了整体流程,不难发现,我们要实现反序列化只有一个问题,就是如何获取密钥,伪造出签名,只要能伪造出签名,我们就可以实现任意文件读取

  • 想要获取签名,上面有一个流程很奇怪

    1
    2
    3
    4
    5
    6
    7
    8
    if(!empty($_POST["nickname"])) {
    $arr = array($_POST["nickname"],$this->eancrykey);
    $data = "Welcome my friend %s";
    foreach ($arr as $k => $v) {
    $data = sprintf($data,$v);
    }
    parent::response($data,"Welcome");
    }

在这里是唯一一个可能输出的密钥的地方,但是在进入第二次sprintf前%s由于被格式化了,就没了,因此不难想到如果$_POST["nickname"]=='%s',那样就可以输出了。

  • 反序列的小阻碍,对于$path的过滤,不过过滤的十分捡漏,即去掉../,但是我们需要../只需要..././=》../就可以了,凑18就更简单了

知识补充

sprintf带来的漏洞问题,往往不是那么明显,而是往往在字符串拼接的时候带来类似的问题,是在不经意间的。并且在sql注入中出现的比较多

函数介绍

sprintf ( string $format [, mixed $... ] ) : string

格式化字符时的Flag
1_1.png
格式化字符类型
1_2.png

以下面这段代码为例子
1_3.png

  • %n$t的意思是传入的第n个参数以t这个数据类型填入这个位置,这里就s代表string,d代表int类型
  • 如果t这个数据类型不存在则会置空

PS:如果是使用双引号写语句,需要转移$字符,原因在于php中只有把单引号的字符串当做纯文本,而使用双引号,则会解析其中的$x变量

注入的发生

我们从下面这一段代码开始看起

1
2
3
4
5
6
7
<?php
$password = addslashes($_GET['password']);
$password = sprintf("and password = '%s'",$password);
$sql = sprintf("select * from user where username = '%s' $password",'admin');
echo $sql;

?>

1_4.png
正常来看已经这个防护的已经很好了,单引号被过滤又是格式化字符进去的
但是通过我们上面的知识基础就可以让单引号逃逸

1_5.png

这里我们关键是我们先传入了password=%1$
进入语句之后就会将语句变成
select * from user where username = '%s' and password = '%1$\''

这时候按照上面的介绍,在下一次格式化的时候第一个参数将以%1$\'的方式进行处理,即将以\的数据类型进行填充,但又由于没有这个数据类型所以被指控,最后就导致了单引号逃逸,引发了sql注入

参考链接

https://paper.seebug.org/386/