復現 Code-Breaking Puzzles 記錄

題目地址:Code-Breaking Puzzles
 
  當時一道題也沒做出來,然後12月初的時候看着其他大佬們的Writeup復現了一波,但也沒寫Writeup,於是這一拖再拖,成功地拖過了一年,希望在今年開個好頭,而且p牛的環境還沒關(感謝p牛!),還有得救,趁機把它補上記錄一下。

easy - function

<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';

if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
	show_source(__FILE__);
} else {
	$action('', $arg);
}

 
  當時看了這道題,發現也就是傳入的GET請求參數action中含有字母數字及下劃線其中一個,就能調用下面的show_source函數了,然而這也並沒什麼用,查了下show_source原來就是一個對文件進行語法高亮顯示的函數,無語…說明解題點應該就不是這個地方,可能是要跳過這個if進到else裏面,然後就卡住了也沒繼續想下去了,就放棄了,然後又去大概地看了下其它的題,然而一道也沒搞出來…所以最後就坐等大佬們的Writeup了…
  後面看了大佬們的Writeup,發現確實是要繞過if,然後對第二個參數arg就可以控制了,就可以任意函數調用了,然後就需要找一個字符來繞過正則,還不能影響函數的調用,能繞過這個正則的字符倒是挺多的,主要是還要不能影響後面的函數,所以就fuzz跑一下:

  這裏就能看到**\(%5c)這個字符就可以達到上面的要求,參數也正常顯示了還沒報錯,那麼問題來了,爲什麼把\(%5c)**反斜槓這個字符加到函數名之前不影響正常調用函數呢?具體原因p牛也給出瞭解釋:

php裏默認命名空間是\,所有原生函數和類都在這個命名空間中。普通調用一個函數,如果直接寫函數名function_name()調用,調用的時候其實相當於寫了一個相對路徑;而如果寫\function_name()這樣調用函數,則其實是寫了一個絕對路徑。如果你在其他namespace裏調用系統類,就必須寫絕對路徑這種寫法。

另附 PHP 手冊 - 命名空間
  
  繞過了正則我們就可以找函數來控制第二個參數了,看了師傅們的Writeup後發現師傅們果然見多識廣,居然發現了create_function這個函數可以進行代碼注入,所以還是要熟悉php及其一些常見的函數漏洞,而且p牛在題目上也給出了提示…
大佬們的對create_function函數的解析:
[科普向] 解析create_function() && 復現wp
PHP create_function()代碼注入
  
  create_function函數的第一個參數是傳入的參數,第二個參數是函數的內容。簡單來說這個create_function函數可以對第二個參數進行閉合然後跳出該函數,從而導致在這個函數後可以進行任意代碼執行,造成了漏洞,所以在php7.2以後的版本中PHP 手冊 - create_function已被棄用,官方也不鼓勵用此函數。

所以最後的playload:
PHP 手冊 - scandir函數查看文件目錄:

http://51.158.75.42:8087/?action=%5ccreate_function&arg=1;}var_dump(scandir("../"));/*


查看flag文件得到flag:

http://51.158.75.42:8087/?action=%5ccreate_function&arg=1;}var_dump(file_get_contents(%22../flag_h0w2execute_arb1trary_c0de%22));/*

easy - pcrewaf

<?php
function is_php($data){
    return preg_match('/<\?.*[(`;?>].*/is', $data);
}

if(empty($_FILES)) {
    die(show_source(__FILE__));
}

$user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']);
$data = file_get_contents($_FILES['file']['tmp_name']);
if (is_php($data)) {
    echo "bad request";
} else {
    @mkdir($user_dir, 0755);
    $path = $user_dir . '/' . random_int(0, 10) . '.php';
    move_uploaded_file($_FILES['file']['tmp_name'], $path);

    header("Location: $path", true, 303);
}

 
  這題看樣子是要上傳php代碼,還要讓is_php()這函數返回false才能跳過if,那麼問題來了,/<\?.*[(`;?>].*/is這個正則表達式能夠匹配以<?開頭,中間或結尾含有( ` ; ? >這五個字符其中任意一個的任意代碼,但php一般都是用;(可加可不加結束符?>,參考:PHP 手冊 - PHP tags)來結尾,貌似有些情況也可以直接用結束符?>來結尾,所以這個正則表達式貌似能把任意的php代碼給匹配得到的啊,如下圖所示,這樣就繞不過啊,真讓人頭大,建議放棄

後面看了大佬們的Writeup後知道了這正則匹配存在回溯限制(見PHP 手冊 - Runtime Configuration),回溯次數超過了它的限制(1000000次)就返回false,利用這點就可以上傳shell了。
參考:
鳥哥的講解:深悉正則(pcre)最大回溯/遞歸限制
p牛的Writeup:PHP利用PCRE回溯次數限制繞過某些安全限制

利用p牛寫的poc就可以得到文件地址了:

然後就同第一題那樣就能拿到flag了:

easy - phplimit

<?php
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
    eval($_GET['code']);
} else {
    show_source(__FILE__);
}

 
  這道題看着代碼挺短,但看到(?R)還有點迷,查了下發現這是正則匹配裏的遞歸模式,下面是遞歸模式的詳解:
鳥哥的講解:PHP正則之遞歸匹配
PHP 手冊 - 遞歸模式
  簡單來說就是當匹配到(?R)的時候又繼續從頭開始匹配,那麼這道題的/[^\W]+\((?R)?\)/這個正則表達式開始是[^\W]能匹配任意字母、數字或下劃線(_),也就相當於[a-zA-Z0-9_],後面的\(就匹配(,然後到(?R)開始從頭匹配,又從[^\W]開始匹配,如果匹配不到就往後面匹配,\)就匹配),所以這個正則表達式相當於匹配不帶參數的函數,也可以是以不帶參數的函數爲參數的函數,類似a(b(c()))這樣的就可以匹配得到。
  但這道題是將傳入的GET請求參數code進行正則匹配後將匹配到的替換爲空(PHP 手冊 - preg_replace),將替換後得到的結果與’;'作對比,如果完全相同則執行傳入的代碼,所以如果要讀取文件路徑這就是個難點,畢竟要傳入不能帶參數的函數才能夠執行…
  看了師傅們的Writeup後知道了get_defined_varsPHP 手冊 - get_defined_vars)這個函數可以獲取全局所有的變量,那我們打印出來看下:

這裏就可以看到GET的第一個值爲我們剛傳進去的code參數,那我們再傳一個參數看看:

參數a也傳進去了,所以在這裏我們可以用current或者reset函數都可以獲取數組的第一個元素的值:

接下來就可以用next函數來獲取該數組的下一個(也就是第二個)元素的值:

這樣,執行該函數就能得到文件路徑及flag了:


 
  差不多了,那就先到這吧…
  
  To be continued?
  …
  Maybe…


復現的時候參考了以下各位師傅們的Writeup:
l3m0n:code-breaking writeup
f1sh:Code-Breaking Puzzles做題記錄
酉酉囧:代碼審計CODE-BREAKING PUZZLES學習記錄
Kingkk:Code-Breaking Puzzles 題解&學習篇
LoRexxar:Code Breaking挑戰賽 Writeup
By七友:Code-Breaking Puzzles做題記錄
Blacsheep:ph師傅的代碼審計星球
fnmsd:Code-Breaking Puzzles 做題記錄
eustiar:代碼審計知識星球

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