图床

首先发现一个任意文件读,但是只能读出源码,读不到 ../../../../../../../../../../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 恢复出视频。挺有意思。