Writer:b1uef0x / Webページ建造途中
あまり時間が取れなかったが、個人参加でwarmup問題が解けたのでまとめておく。
Read /flag.txt on the server.
GitのWebサイトからファイルを取得するphpを利用して、Webサーバーからflag.txtを取得する問題。
phpファイルが配布されるので中身を確認する。
<?php
function h($s) { return htmlspecialchars($s); }
function craft_url($service, $owner, $repo, $branch, $file) {
if (strpos($service, "github") !== false) {
/* GitHub URL */
return $service."/".$owner."/".$repo."/".$branch."/".$file;
} else if (strpos($service, "gitlab") !== false) {
/* GitLab URL */
return $service."/".$owner."/".$repo."/-/raw/".$branch."/".$file;
} else if (strpos($service, "bitbucket") !== false) {
/* BitBucket URL */
return $service."/".$owner."/".$repo."/raw/".$branch."/".$file;
}
return null;
}
$service = empty($_GET['service']) ? "" : $_GET['service'];
$owner = empty($_GET['owner']) ? "ptr-yudai" : $_GET['owner'];
$repo = empty($_GET['repo']) ? "ptrlib" : $_GET['repo'];
$branch = empty($_GET['branch']) ? "master" : $_GET['branch'];
$file = empty($_GET['file']) ? "README.md" : $_GET['file'];
if ($service) {
$url = craft_url($service, $owner, $repo, $branch, $file);
if (preg_match("/^http.+\/\/.*(github|gitlab|bitbucket)/m", $url) === 1) {
$result = file_get_contents($url);
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>GitFile Explorer</title>
<link rel="stylesheet" href="https://cdn.simplecss.org/simple-v1.css">
</head>
<body>
<header>
<h1>GitFile Explorer API Test</h1>
<p>Simple API to download files on GitHub/GitLab/BitBucket</p>
</header>
<main>
<form method="GET" action="/">
<label for="service">Service: </label>
<select id="service" name="service" autocomplete="off">
<option value="https://raw.githubusercontent.com" <?= strpos($service, "github") === false ? "" : 'selected="selected"' ?>>GitHub</option>
<option value="https://gitlab.com" <?= strpos($service, "gitlab") === false ? "" : 'selected="selected"' ?>>GitLab</option>
<option value="https://bitbucket.org" <?= strpos($service, "bitbucket") === false ? "" : 'selected="selected"' ?>>BitBucket</option>
</select>
<br>
<label for="owner">GitHub ID: </label>
<input id="owner" name="owner" type="text" placeholder="Repository Owner" value="<?= h($owner); ?>">
<br>
<label for="repo">Repository Name: </label>
<input id="repo" name="repo" type="text" placeholder="Repository Name" value="<?= h($repo); ?>">
<br>
<label for="branch">Branch: </label>
<input id="branch" name="branch" type="text" placeholder="Branch Name" value="<?= h($branch); ?>">
<br>
<label for="file">File Path: </label>
<input id="file" name="file" type="text" placeholder="README.md" value="<?= h($file); ?>">
<br>
<input type="submit" value="Download">
</form>
<?php if (isset($result)) { ?>
<br>
<?php if ($result === false) { ?>
<p>Not Found :(</p>
<?php } else {?>
<textarea rows="20" cols="40"><?= h($result); ?></textarea>
<?php } ?>
<?php } ?>
</main>
<footer>
<p>zer0pts CTF 2022</p>
</footer>
</body>
</html>
Webサイトからのファイルの取得にfile_get_contentsを使用しており、ここに自サーバーへのディレクトリトラバーサル攻撃を行えばよい。
file_get_contentsに渡すURLはチェックされており、Serviceにgithub,gitlab,bitbucketの何れかを含んだ上で、URLが以下の正規表現を満たす必要がある。
preg_match("/^http.+\/\/.*(github|gitlab|bitbucket)/m", $url)
というわけでこれらを踏まえて正規表現をパスする入力値は以下の通り。
参照するURLは http/..//github/..///////../../../flag.txt
になり、flagを取得できる。
zer0pts{foo/bar/../../../../../directory/traversal}
I invented Anti-Fermat Key Generation for RSA cipher since I'm scared of the Fermat's Factorization Method.
時期的に某製品のライブラリのRSA実装に脆弱性があった件のネタか?
Pythonコードと出力結果が配布される。
task.pyfrom Crypto.Util.number import isPrime, getStrongPrime
from gmpy import next_prime
from secret import flag
# Anti-Fermat Key Generation
p = getStrongPrime(1024)
q = next_prime(p ^ ((1<<1024)-1))
n = p * q
e = 65537
# Encryption
m = int.from_bytes(flag, 'big')
assert m < n
c = pow(m, e, n)
print('n = {}'.format(hex(n)))
print('c = {}'.format(hex(c)))
output.txtn = 0x1ffc7dc6b9667b0dcd00d6ae92fb34ed0f3d84285364c73fbf6a572c9081931be0b0610464152de7e0468ca7452c738611656f1f9217a944e64ca2b3a89d889ffc06e6503cfec3ccb491e9b6176ec468687bf4763c6591f89e750bf1e4f9d6855752c19de4289d1a7cea33b077bdcda3c84f6f3762dc9d96d2853f94cc688b3c9d8e67386a147524a2b23b1092f0be1aa286f2aa13aafba62604435acbaa79f4e53dea93ae8a22655287f4d2fa95269877991c57da6fdeeb3d46270cd69b6bfa537bfd14c926cf39b94d0f06228313d21ec6be2311f526e6515069dbb1b06fe3cf1f62c0962da2bc98fa4808c201e4efe7a252f9f823e710d6ad2fb974949751
c = 0x60160bfed79384048d0d46b807322e65c037fa90fac9fd08b512a3931b6dca2a745443a9b90de2fa47aaf8a250287e34563e6b1a6761dc0ccb99cb9d67ae1c9f49699651eafb71a74b097fc0def77cf287010f1e7bd614dccfb411cdccbb84c60830e515c05481769bd95e656d839337d430db66abcd3a869c6348616b78d06eb903f8abd121c851696bd4cb2a1a40a07eea17c4e33c6a1beafb79d881d595472ab6ce3c61d6d62c4ef6fa8903149435c844a3fab9286d212da72b2548f087e37105f4657d5a946afd12b1822ceb99c3b407bb40e21163c1466d116d67c16a2a3a79e5cc9d1f6a1054d6be6731e3cd19abbd9e9b23309f87bfe51a822410a62
件の脆弱性はpに対してq=next_prime(p)としていることからフェルマーの素因数分解法で簡単に解けてしまうが、この問題ではp=getStrongPrime(1024)に対してq=next_prime(p^((1<<1024)-1))で計算している。(^はXOR演算)
f = ((1<<1024)-1)としておく。これは0xffff...ffffという数で、この数に限り、f^p=f-pになる。q≒f-pになるので、n=p(f-p)、つまりp^2 - p*f + n = 0の2次方程式の形になり、pについて解くことができる。p = (f±√(f**2 - 4*n))/2
大体のpの値がわかったので、あとは1ずつずらしながらnで割り切れるものを探すとそのうち当たる。
以下のsolverを書いた。
from Crypto.Util.number import bytes_to_long, long_to_bytes
import gmpy2
import math
n = 0x1ffc7dc6b9667b0dcd00d6ae92fb34ed0f3d84285364c73fbf6a572c9081931be0b0610464152de7e0468ca7452c738611656f1f9217a944e64ca2b3a89d889ffc06e6503cfec3ccb491e9b6176ec468687bf4763c6591f89e750bf1e4f9d6855752c19de4289d1a7cea33b077bdcda3c84f6f3762dc9d96d2853f94cc688b3c9d8e67386a147524a2b23b1092f0be1aa286f2aa13aafba62604435acbaa79f4e53dea93ae8a22655287f4d2fa95269877991c57da6fdeeb3d46270cd69b6bfa537bfd14c926cf39b94d0f06228313d21ec6be2311f526e6515069dbb1b06fe3cf1f62c0962da2bc98fa4808c201e4efe7a252f9f823e710d6ad2fb974949751
c = 0x60160bfed79384048d0d46b807322e65c037fa90fac9fd08b512a3931b6dca2a745443a9b90de2fa47aaf8a250287e34563e6b1a6761dc0ccb99cb9d67ae1c9f49699651eafb71a74b097fc0def77cf287010f1e7bd614dccfb411cdccbb84c60830e515c05481769bd95e656d839337d430db66abcd3a869c6348616b78d06eb903f8abd121c851696bd4cb2a1a40a07eea17c4e33c6a1beafb79d881d595472ab6ce3c61d6d62c4ef6fa8903149435c844a3fab9286d212da72b2548f087e37105f4657d5a946afd12b1822ceb99c3b407bb40e21163c1466d116d67c16a2a3a79e5cc9d1f6a1054d6be6731e3cd19abbd9e9b23309f87bfe51a822410a62
e = 65537
f = ((1<<1024)-1)
p = (f+gmpy2.isqrt(f**2- 4*n))//2
while not n % p==0:
p += 1
q = n//p
def lcm(p, q):
return (p * q) // math.gcd(p, q)
def _etension_euclid(x,y):
c0, c1 = x, y
a0, a1 = 1, 0
b0, b1 = 0, 1
while c1 != 0:
mm = c0 % c1
qq = c0 // c1
c0, c1 = c1, mm
a0, a1 = a1, (a0 - qq * a1)
b0, b1 = b1, (b0 - qq * b1)
return c0, a0, b0
l = lcm(p - 1, q - 1)
_c, a, _b = _etension_euclid(e, l)
d = a % l
print(long_to_bytes((pow(c,d,n))))
無事pとqを取得できてflagを取得。
Good job! Here is the flag:
+-----------------------------------------------------------+
| zer0pts{F3rm4t,y0ur_m3th0d_n0_l0ng3r_w0rks.y0u_4r3_f1r3d} |
+-----------------------------------------------------------+
service charge
Windows用の実行ファイルchall.exeが配布される。実行するとFLAGの入力を求められ、失敗するとWrong...になる。
実行ファイルをGhidraに読ませてWrongがあるところを見てみる。
undefined8 FUN_0040167c(void)
{
ulonglong uVar1;
undefined8 local_68;
undefined8 local_60;
undefined8 local_58;
undefined8 local_50;
undefined8 local_48;
undefined8 local_40;
undefined8 local_38;
undefined8 local_30;
DWORD local_1c;
HANDLE local_18;
HANDLE local_10;
FUN_004018b0();
local_68 = 0;
local_60 = 0;
local_58 = 0;
local_50 = 0;
local_48 = 0;
local_40 = 0;
local_38 = 0;
local_30 = 0;
local_10 = GetStdHandle(0xfffffff6);
local_18 = GetStdHandle(0xfffffff5);
if ((local_10 == (HANDLE)0xffffffffffffffff) || (local_18 == (HANDLE)0xffffffffffffffff)) {
ExitProcess(1);
}
WriteConsoleA(local_18,"FLAG: ",6,&local_1c,(LPVOID)0x0);
ReadConsoleA(local_10,&local_68,0x3f,&local_1c,(PCONSOLE_READCONSOLE_CONTROL)0x0);
uVar1 = FUN_00401550((int)register0x00000020 + -0x68);
if ((int)uVar1 == 0) {
WriteConsoleA(local_18,"Wrong...\n",9,&local_1c,(LPVOID)0x0);
}
else {
WriteConsoleA(local_18,"Correct!\n",9,&local_1c,(LPVOID)0x0);
}
CloseHandle(local_10);
CloseHandle(local_18);
return 0;
}
直前のFUN_00401550でフラグを判定しているようだ。
ulonglong FUN_00401550(int param_1)
{
DWORD in_stack_ffffffffffffffa0;
undefined4 in_stack_ffffffffffffffa4;
DWORD in_stack_ffffffffffffffa8;
undefined4 in_stack_ffffffffffffffac;
undefined4 in_stack_ffffffffffffffb0;
uint local_4c;
LPCSTR in_stack_ffffffffffffffb8;
LPDWORD in_stack_ffffffffffffffc0;
LPCSTR in_stack_ffffffffffffffc8;
LPCSTR in_stack_ffffffffffffffd0;
SC_HANDLE local_28;
SC_HANDLE local_20;
int local_14;
uint local_10;
uint local_c;
local_c = 1;
local_4c = 0x20;
CreateServiceA((SC_HANDLE)&local_20,(LPCSTR)0x0,(LPCSTR)0x0,0x18,0xf0000000,
in_stack_ffffffffffffffa0,in_stack_ffffffffffffffa8,
(LPCSTR)CONCAT44(0x20,in_stack_ffffffffffffffb0),in_stack_ffffffffffffffb8,
in_stack_ffffffffffffffc0,in_stack_ffffffffffffffc8,in_stack_ffffffffffffffd0,
(LPCSTR)local_28);
local_10 = 0;
while (local_10 < local_4c) {
ChangeServiceConfigA
(local_20,0x800c,0,0,&stack0xffffffffffffffd8,
(LPCSTR)CONCAT44(in_stack_ffffffffffffffa4,in_stack_ffffffffffffffa0),
(LPDWORD)CONCAT44(in_stack_ffffffffffffffac,in_stack_ffffffffffffffa8),
(LPCSTR)CONCAT44(local_4c,in_stack_ffffffffffffffb0),in_stack_ffffffffffffffb8,
(LPCSTR)in_stack_ffffffffffffffc0,in_stack_ffffffffffffffc8);
ControlService(local_28,local_10 * 2 + param_1,(LPSERVICE_STATUS)0x2);
CloseServiceHandle(local_28);
local_14 = 0;
while (local_14 < 0x20) {
local_c = local_c & (&stack0xffffffffffffffb8)[local_14] ==
(&DAT_00403020)[(int)(local_14 + local_10 * 0x20)];
local_14 = local_14 + 1;
}
local_10 = local_10 + 1;
}
return (ulonglong)local_c;
}
どのような関数が呼び出されているか知りたいが、静的解析ではよくわからない。x64dbgで該当する部分を調べてみる。
デバッガー上からは呼び出されているAPI関数の名前が確認できる。ハッシュ計算を行っているようだ。
CryptCreateHash関数の引数を上記のGhidraの逆コンパイル結果で確認すると、2番めの引数に0x800cが入っている。これは( https://docs.microsoft.com/ja-jp/windows/win32/seccrypto/alg-id )で確認するとSHA256である。
whileループで何度もハッシュ値を計算していることから、入力データを分割していると考えて入力文字数を変えながらループする回数を確認すると、どうやら2文字ずつ計算していることがわかる。
FLAGにとりあえずzer0ptsと入れた状態で、ハッシュ値を計算するCryptHashData関数まで実行すると、スタック上にハッシュ値のようなものが出力される。
試しに入力文字列の最初のzeのSHA256を取ると33129567e0bd787efb15a26307e5311e06ba66e3b8dbc2206ad59f99780a4d78になり、スタック上のデータと一致する。これらのデータはDAT_00403020から記録されており、ここにフラグ文字列を2文字ずつSHA256ハッシュをとった値が入っているようだ。
実行ファイルのハッシュ値格納部分のバイナリ列を取り出し、任意の2文字のSHA256をとって一致する値を探すsolverを作成した。
import hashlib
import string
f = open("chall.exe","rb")
data = f.read()[0x2220:0x2220+32*0x20]
hashs = []
flags = []
for i in range(0x20):
hashs.append(data[32*i:32*(i+1)].hex())
flags.append("**")
strs = string.printable
for s1 in strs:
for s2 in strs:
h = hashlib.sha256((s1+s2).encode()).hexdigest()
if h in hashs:
flags[hashs.index(h)] = s1+s2
print("".join(flags))
実行するとflagのほとんどが得られる。zer0pts{m0d1fy1ng_PE_1mp0rts**s_4n_34sy_0bfusc4t10n}
何故か1箇所だけ失敗しているが、ここまでわかればisっぽいので、勘でflagを入手。
zer0pts{m0d1fy1ng_PE_1mp0rts_1s_4n_34sy_0bfusc4t10n}