题目描述:

This is a very unexpected gig for me. However, I'm busy with other projects so can you please give me a hand to test this. For free, of course. 🙂
Files: https://dctf.def.camp/dctf-18-quals-81249812/get-admin.zip
Target: https://admin.dctfq18.def.camp/
Author: Andrei

页面是个很简单的登录窗口,可以注册新用户。

登录进来发现就一行字"Try harder",除此之外啥都没有,还是看源码吧。

在admin.php中发现

当userid=1时可以得到flag,否则就是之前见到的try harder。

再看一下其他代码文件,发现最主要的就是config.php和index.php

config.php:

<?php 

$host = "localhost";  
$username = "root";  
$password = "hackingduality";  
$database = "bad-login"; 

define('FLAG', 'DCTF{HIDDEN}');
define('AES_KEY', '');
define('AES_IV', '');

$db = new PDO("mysql:host=$host; dbname=$database", $username, $password);  
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);  

include_once('auth.lib.php');
session_start();

function compress($arr) {
    return implode('÷', array_map(function ($v, $k) { return $k.'¡'.$v; }, $arr, array_keys($arr) ));
}
  
function decompress($cookie) {
    if(preg_match('/[^\x00-\x7F]+\ *(?:[^\x00-\x7F]| )*/im',$cookie, $m) == 0) {
        echo('Decryption error (1).');
        return false;
    }

    $t = explode("÷", $cookie);

    $arr = [];
    foreach($t as $el) { 
        $el = explode("¡", $el); 
        $arr[$el[0]] = $el[1];
    } 

    if(!isset($arr['checksum'])) {
        echo('Decryption error (2).');
        return false;
    }

    $checksum = intval($arr['checksum']);
    unset($arr['checksum']);
    $cookie = compress($arr);
    if($checksum != crc32($cookie)) {
        echo('Decryption error (3).');
        return false;
    } 

    return $arr;
}

function encryptCookie($arr) {
    $cookie = compress($arr);
    $arr['checksum'] = crc32($cookie); 
    return encrypt(compress($arr), AES_KEY, AES_IV);
}

function decryptCookie($cypher) { 
    return decompress(decrypt($cypher, AES_KEY, AES_IV));
}

function encrypt($plaintext, $key, $iv) {
    $length     = strlen($plaintext);
    $ciphertext = openssl_encrypt($plaintext, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
    return base64_encode($ciphertext) . sprintf('%06d', $length);
}

function decrypt($ciphertext, $key, $iv) {
    $length     = intval(substr($ciphertext, -6, 6));
    $ciphertext = substr($ciphertext, 0,-6);
    $output     = openssl_decrypt(base64_decode($ciphertext), 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
    if($output == FALSE) {
        echo('Decryption error (0).');
        die();
    }
    return substr($output, 0, $length);
}


index.php:


<?php 

include_once('config.php');

if (!isset($_SESSION['userid'])) {
    if(!empty($_COOKIE['user'])) {
        $u = decryptCookie($_COOKIE['user']);

        if($u['id'] > 0) {
            $_SESSION['userid'] = $u['id'];
            header("Location: /admin.php");
            exit;
        } 
        die('Invalid cookie.');
    } else if(isset($_POST['username'], $_POST['password'])) {
        $auth = new AuthLib($db);
        $userid = (int) $auth->authenticate($_POST['username'], $_POST['password']);
        if ($userid) {
            $q = $db->query('SELECT * FROM `users` where id='.$userid);
            $row = $q->fetch(\PDO::FETCH_ASSOC);

            $_SESSION['userid'] = $userid;
         
            setcookie('user',encryptCookie([
                'id' => $userid,
                'username' => $_POST['username'],
                'email' => $row['email'], 
            ]), time()+60*60*24*30);
            
            header("Location: /admin.php");
            exit;
        }
    }
    require_once('login.php');
} else {
    require_once('admin.php');
}

从index.php中可以看到,在没有userid的情况下,如果有cookie,则会调用decryptCookie将cookie解密后分配id;如果没有cookie,则会调用encryptCookie将id,username以及email进行加密得到cookie,index.php的作用大致如此了,关键看config.php关于如何加解密

关于加密主要有下面三个函数


function encryptCookie($arr) {
    $cookie = compress($arr);
    $arr['checksum'] = crc32($cookie); 
    return encrypt(compress($arr), AES_KEY, AES_IV);
}

function encrypt($plaintext, $key, $iv) {
    $length     = strlen($plaintext);
    $ciphertext = openssl_encrypt($plaintext, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
    return base64_encode($ciphertext) . sprintf('%06d', $length);
}

function compress($arr) {
    return implode('÷', array_map(function ($v, $k) { return $k.'¡'.$v; }, $arr, array_keys($arr) ));
}

compress将传入的数组序列化成字符串,其中键与值用¡分隔(不是i),键值对之间用÷分隔,比如

$test = [
            'id' => 1,
            'username' => 'Eustiar',
            'email' => 'Eustiar@Eustiar.com'
        ];

会被转化为:id¡1÷username¡Eustiar÷email¡Eustiar@Eustiar.com

encrypt有三个参数:明文,密钥key,用于加密的初始向量iv。使用AES-128-CBC加密明文,并且将密文base64转码,最后将原本明文的长度附在最后(格式为固定长度6,十进制,前面填0,比如000068)。

encryptCookie先调用compress将数组arr转化成字符串,利用这个字符串计算crc32校验和,并将校验和值加入数组,最后依次调用compress、encrypt加密。

关于解密同样有三个函数:


function decryptCookie($cypher) { 
    return decompress(decrypt($cypher, AES_KEY, AES_IV));
}

function decrypt($ciphertext, $key, $iv) {
    $length     = intval(substr($ciphertext, -6, 6));
    $ciphertext = substr($ciphertext, 0,-6);
    $output     = openssl_decrypt(base64_decode($ciphertext), 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
    if($output == FALSE) {
        echo('Decryption error (0).');
        die();
    }
    return substr($output, 0, $length);
}

function decompress($cookie) {
    if(preg_match('/[^\x00-\x7F]+\ *(?:[^\x00-\x7F]| )*/im',$cookie, $m) == 0) {
        echo('Decryption error (1).');
        return false;
    }

    $t = explode("÷", $cookie);

    $arr = [];
    foreach($t as $el) { 
        $el = explode("¡", $el); 
        $arr[$el[0]] = $el[1];
    } 

    if(!isset($arr['checksum'])) {
        echo('Decryption error (2).');
        return false;
    }

    $checksum = intval($arr['checksum']);
    unset($arr['checksum']);
    $cookie = compress($arr);
    if($checksum != crc32($cookie)) {
        echo('Decryption error (3).');
        return false;
    } 

    return $arr;
}

decryptCookie调用另外两个函数,不用多看

decrypt将密文(去掉表示长度的最后六位length)利用密钥和iv解密(然而这两个值我们都不知道)以及base64解码,返回明文从0开始长度为length的消息。看到这里脑中应该有点想法了,如果这个length被修改了会怎么样?

decompress将decrypt后的明文还原成数组,然后会利用校验和检查明文是否被更改,若无修改则返回这个数组。

代码分析差不多就到这里,接下来是利用思路。

为了得到flag,我们必须要修改cookie,以便服务器将它解密后得到的userid为1。cookie是由id,username,email以及checksum加密后再附上长度得到的。

经过测试发现,注册时对注册信息并没有检测,可以使用任意字符作为username,那么考虑如果使用这样的username注册(我永远喜欢02.jpg):

Eustiar02÷id¡1

使用这样的username,在加密的时候会使用这样的明文:

id¡?÷username¡Eustiar02÷id¡1÷email¡Eustiar02@Eustiar02.com÷checksum¡34773466

?不一定是一位,因为不知道id是多少,34773466是正常计算的crc32。

这样会有两个id,但是注意到decompress中还原数组时的那个foreach循环,显然后一个id会覆盖前一个,这样我们的id就变成1了。

不过这样还没完,因为这样一来checksum会出现问题,因为再次compress的字符串显然与之前不同,进而重新计算crc32也会不同。

那么可不可以与id一样,把checksum也放在username里呢?

显然不行,因为无论放在username还是email里,自己计算的checksum都会被后来的checksum覆盖掉。

这里就要考虑前面提到的length了,decompress的明文是由decrypt解密的,但在返回时是截取了原本明文长度为length的子串,如果是正常情况是不会出问题的,返回整个串。但是这个length恰巧是可以由我们控制的,也就是说我们可以通过修改length来截取明文,这样可以将系统给我们添加的checksum直接给丢掉。

考虑这样的username:

Eustiar02÷checksum¡1436923376÷id¡1

得到这样的明文

id¡?÷username¡Eustiar02÷checksum¡1436923376÷id¡1÷email¡Eustiar02@Eustiar02.com÷checksum¡???????

然后我们通过修改length,使传入decompress的明文变为

id¡?÷username¡Eustiar02÷checksum¡1436923376÷id¡1

(email留不留都可以,并没有影响,只不过checksum不同)
其中1436923376是'id¡1÷username¡Eustiar02'的checksum,这样就可以让我们的id变成1,得到flag。

接下来实际操作一下

注册后登陆

可以看到我们正常的cookie

退出后把刚才的cookie添加进去,如果将长度修改一下,不出所料会报错

‘id¡1÷username¡Eustiar02÷checksum¡1436923376÷id¡1’的长度为55,因为不知道实际的id是多少位,所以可以从55开始尝试,尝试到58的时候发现成功了,显然id是四位的。

flag:

打赏作者