题目描述:
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: